From the Library of Shirong Chen
The Merb Way
From the Library of Shirong Chen
Addison-Wesley Professional Ruby Series Obie Fernandez, Series Editor
The Addison-Wesley Professional Ruby Series provides readers with practical, people-oriented, and in-depth information about applying the Ruby platform to create dynamic technology solutions. The series is based on the premise that the need for expert reference books, written by experienced practitioners, will never be satisfied solely by blogs and the Internet.
informit.com/ruby
From the Library of Shirong Chen
The Merb Way
Foy Savas
Upper Saddle River, NJ • Boston • Indianapolis • San Francisco New York • Toronto • Montreal • London • Munich • Paris • Madrid Capetown • Sydney • Tokyo • Singapore • Mexico City
From the Library of Shirong Chen
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals. The author and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein. The publisher offers excellent discounts on this book when ordered in quantity for bulk purchases or special sales, which may include electronic versions and/or custom covers and content particular to your business, training goals, marketing focus, and branding interests. For more information, please contact:
Acquisitions Editor Debra Williams Cauley Development Editor Michael Thurston Managing Editor John Fuller Project Editor Anna Popick
U.S. Corporate and Government Sales (800) 382-3419
[email protected] Copy Editor Barbara Wood
For sales outside the United States please contact:
Indexer Richard Evans
International Sales
[email protected] Proofreader Barbara Wood
Visit us on the Web: informit.com/aw Library of Congress Cataloging-in-Publication Data Savas, Foy. The Merb way / Foy Savas. p. cm. ISBN 978-0-321-60638-9 (pbk. : alk. paper) 1. Web site development. 2. Merb (Electronic resource) I. Title. TK5105.888.S2775 2009 006.7’6—dc22
Editor-in-Chief Mark Taub
Technical Reviewers Matthew Knox Jen Lindner Editorial Assistant Kim Boedigheimer Cover Designer Chuti Prasertsith
2009013263
c 2009 Pearson Education, Inc. Copyright All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, write to:
Compositor ITC
Pearson Education, Inc. Rights and Contracts Department 501 Boylston Street, Suite 900 Boston, MA 02116 Fax: (617) 671-3447 The code in this book may be distributed only subject to the terms and conditions set forth in the MIT License. The MIT License reads: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation to the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. ISBN-13: 978-0-321-60638-9 ISBN-10: 0-321-60638-8 Text printed in the United States on recycled paper at Donnelley in Crawfordsville, Indiana. First printing, June 2009
From the Library of Shirong Chen
To Sophia —I think we can afford to eat this month
From the Library of Shirong Chen
This page intentionally left blank
From the Library of Shirong Chen
Contents
Foreword xix Acknowledgments xxi Merb pastie xxiii Introduction xxvii About the author xxxi
Chapter 1 1.1 1.2
1.3
1.4
Fundamentals 1
Generating a new application 1 The layout of a Merb application 2 1.2.1 Very flat layout 3 1.2.2 Flat layout 6 1.2.3 Standard layout 8 Interactive Merb 9 1.3.1 Console methods 10 1.3.2 Sandboxing 11 Merb configuration 12 1.4.1 Init script 12 1.4.1.1 Gems and load path 12 1.4.1.2 Dependencies 13 after_app_loads 14 1.4.1.3 1.4.1.4 Template engine 14 1.4.1.5 Basic configuration 14
vii From the Library of Shirong Chen
viii
Contents
1.5
1.6
1.4.1.6 ORM options 14 1.4.1.7 Testing options 15 1.4.1.8 Inflector 15 1.4.2 Environments 16 1.4.3 Router 17 1.4.4 Logging 17 1.4.4.1 Using logging 18 1.4.4.2 Viewing logs 18 1.4.5 Database 19 Understanding the Merb stack 20 1.5.1 Extlib 20 1.5.1.1 ObjectSpace 20 Class 20 1.5.1.2 Object 21 1.5.1.3 1.5.1.4 String 21 Time 22 1.5.1.5 DateTime 22 1.5.1.6 1.5.1.7 Pathname 22 1.5.1.8 blank? 22 Logger 22 1.5.1.9 1.5.1.10 Mash 22 1.5.1.11 SimpleSet 23 1.5.1.12 VirtualFile 23 1.5.1.13 LazyArray 23 1.5.1.14 Hook 23 1.5.1.15 Pooling 23 1.5.2 Rack 23 1.5.2.1 Adapter options 23 1.5.2.2 How Rack works 24 1.5.3 ORMs 26 1.5.4 Plugins 26 An overview of Merb internals 27 1.6.1 Boot loaders 27 1.6.1.1 BootLoader::Logger 27 1.6.1.2 BootLoader::DropPidFile 28 BootLoader::Defaults 28 1.6.1.3 BootLoader::BuildFramework 28 1.6.1.4
From the Library of Shirong Chen
Contents
1.7
ix
1.6.1.5 BootLoader::Dependencies 29 1.6.1.6 BootLoader::MixinSession 29 BootLoader::BeforeAppHooks 29 1.6.1.7 BootLoader::LoadClasses 29 1.6.1.8 BootLoader::Templates 29 1.6.1.9 1.6.1.10 BootLoader::MimeTypes 30 1.6.1.11 BootLoader::Cookies 30 1.6.1.12 BootLoader::SetupSession 30 1.6.1.13 BootLoader::AfterAppLoads 30 1.6.1.14 BootLoader::SetupStubClasses 31 1.6.1.15 BootLoader::ChooseAdapter 31 1.6.1.16 BootLoader::StartWorkerThread 31 1.6.1.17 BootLoader::RackUpApplication 31 1.6.1.18 BootLoader::ReloadClasses 32 1.6.1.19 BootLoader::ReloadTemplates 32 1.6.2 Server 32 1.6.3 Requests 33 1.6.4 Router 33 1.6.5 Dispatcher 33 1.6.6 Controllers 34 1.6.7 Sessions 34 1.6.8 Worker 34 Conclusion 35
Chapter 2 2.1
2.2
Routing 37
How Merb routing works 38 2.1.1 Route conditions 39 2.1.1.1 Route method 39 2.1.1.2 Route path 40 2.1.1.3 Other conditions 41 2.1.2 Route parameters 42 Router configuration 43 2.2.1 The router file 43 2.2.2 The prepare block 43 2.2.3 Route order 44 2.2.4 Adding routes later on 44
From the Library of Shirong Chen
x
Contents
2.3
2.4
2.5
2.6
2.7
Checking routes 45 2.3.1 Listing routes 45 2.3.1.1 Using interactive Merb 45 2.3.1.2 Using the audit routes rake task 48 2.3.2 Trying the router 49 2.3.2.1 For URL recognition 49 2.3.2.2 For URL generation 50 Match rules 50 2.4.1 Literal matching 50 2.4.2 Symbolic matches 51 2.4.2.1 Automatic parameters 51 2.4.2.2 Flexible segmentation 52 2.4.2.3 Segment-specific regular expressions 52 2.4.3 Optional matches 52 2.4.4 Full regular expressions 53 2.4.5 Deferred routes 53 Registering routes 54 2.5.1 Using to 54 2.5.2 Using with 55 2.5.3 Using register 55 2.5.4 Redirects 56 2.5.5 Using symbols 56 2.5.6 Using other captures 56 Other route settings 57 2.6.1 Setting defaults 57 2.6.2 Named routes 57 2.6.3 Setting prefixes 58 2.6.3.1 Name prefix 58 2.6.3.2 Controller prefix 59 2.6.3.3 Namespaces 59 2.6.4 Fixatable routes 59 Resource routes 60 2.7.1 Standard resources routing 60 2.7.1.1 Index 61 2.7.1.2 New 62 2.7.1.3 Create 62 2.7.1.4 Edit 62
From the Library of Shirong Chen
Contents
2.8
xi
2.7.1.5 Update 62 2.7.1.6 Destroy 62 2.7.1.7 Show 62 2.7.2 Singular resource routing 2.7.3 Using identify 63 Conclusion 64
Chapter 3 3.1
3.2
3.3
3.4
62
Controllers 65
From request to controller 65 3.1.1 A simple application 66 3.1.2 How requests get dispatched 66 3.1.3 The controller’s perspective 67 The controller classes 68 3.2.1 The abstract controller 69 3.2.1.1 Class methods 69 3.2.1.2 Instance methods 70 3.2.2 The Merb controller 70 3.2.2.1 Class methods 70 3.2.2.2 Instance methods 72 3.2.3 The application controller 72 3.2.4 The exceptions controller 74 3.2.5 Other controllers 74 Custom controller classes 75 3.3.1 Controller location 75 3.3.2 Naming controllers 76 3.3.3 Organizing controller methods 76 3.3.3.1 Sharing a nonaction method 77 3.3.3.2 Making methods available to subclasses 3.3.3.3 Increasing readability of an action 78 3.3.4 Setting callable actions 79 3.3.4.1 Hiding an action 79 3.3.4.2 Showing an action 79 Filters 79 3.4.1 Before filters 80 3.4.2 After filters 81 3.4.3 Filter options 82
77
From the Library of Shirong Chen
xii
Contents
3.5
3.6
3.7
3.8 3.9
3.10
3.4.4 Skipping filters 83 Redirects 84 3.5.1 A redirect caveat 84 3.5.2 Redirects after POST requests 85 3.5.3 Redirecting in before filters 86 Exceptions 87 3.6.1 Raising an exception 87 3.6.2 Controller exceptions 87 3.6.3 The exceptions controller 88 Rendering templates 89 3.7.1 How templates are compiled 89 3.7.2 Basic rendering 91 3.7.3 Template 91 3.7.4 Formats 92 3.7.5 Status 94 3.7.6 Layout 94 display 95 3.7.7 3.7.8 render_chunked 96 3.7.9 render_deferred 96 3.7.10 render_then_call 97 run_later 97 Sending and streaming 97 3.9.1 Sending files 98 3.9.2 Streaming files 98 Conclusion 99
Chapter 4 4.1
4.2
Views 101
ERB 101 4.1.1 Basic delimiters 102 4.1.2 Removing whitespace 102 4.1.3 Comments 103 4.1.4 Merb’s block-aware enhancer Haml 105 4.2.1 Tags 105 4.2.2 Indentation 106 4.2.3 IDs and classes 106
103
From the Library of Shirong Chen
Contents
4.3 4.4 4.5
xiii
4.2.4 Attributes 107 4.2.5 Interpreting lines 107 4.2.6 Outputting lines 107 4.2.7 Outputting string lines 108 4.2.8 Sanitized lines 108 4.2.9 Preserving whitespace 108 4.2.10 Filters 109 Merb view templates 109 Partials 111 Conclusion 112
Chapter 5 5.1 5.2 5.3
5.4
5.5
Models 113
Configuration 113 Model classes 115 Properties 118 5.3.1 Database storage 120 5.3.1.1 Automigrating the DB schema 121 5.3.2 Defining properties 127 5.3.2.1 Property types 127 5.3.2.2 Option hash 131 Associations 132 5.4.1 Belongs to 133 5.4.2 Has 135 5.4.3 Has through 138 CRUD basics 140 5.5.1 Creating records 140 5.5.2 Retrieving records 142 5.5.2.1 Special query parameters 145 5.5.2.2 Lazy loading of collections 146 5.5.2.3 Lazy loading of properties 148 5.5.2.4 Strategic eager loading 150 5.5.3 Updating records 152 5.5.3.1 Using update_attributes 152 5.5.3.2 Original and dirty attribute values 153 5.5.4 Destroying records 154
From the Library of Shirong Chen
xiv
Contents
5.6 5.7
5.8
Hooks Plugins 5.7.1 5.7.2 5.7.3 5.7.4
154 155 Extra property types 155 Timestamps 156 Aggregates 158 Validations 162 5.7.4.1 Conditions 163 5.7.4.2 Contexts 164 5.7.4.3 Errors 164 Conclusion 165
Chapter 6 6.1 6.2
6.3
6.4 6.5 6.6
6.7
Truncate helper 167 Numeric helpers 168 6.2.1 Two digits 168 6.2.2 Minutes to hours 169 6.2.3 Currency strings 169 Date and time helpers 171 6.3.1 Formats 171 6.3.2 Ordinals 173 6.3.3 Time DSL 174 6.3.4 Relative time 175 Cycle helper 176 Tag helpers 177 Form helpers 179 6.6.1 Builders 179 6.6.2 Helpers 187 Conclusion 192
Chapter 7 7.1
7.2 7.3
Helpers 167
Slices 193
Slice development 193 7.1.1 Generating slices 193 7.1.2 Running slices 196 7.1.3 Building slices 199 7.1.4 Slice controllers 200 Slice usage 201 Conclusion 203
From the Library of Shirong Chen
Contents
xv
Chapter 8 8.1 8.2 8.3
8.4 8.5 8.6
How sessions work 205 Configuration 206 Storing sessions 207 8.3.1 Session containers 207 8.3.2 Session store containers 208 8.3.3 Session storage mechanisms 211 8.3.3.1 Cookie sessions 211 8.3.3.2 Memory sessions 214 8.3.3.3 Memcached sessions 216 8.3.3.4 DataMapper sessions 217 Request access 219 Controller access 220 Conclusion 220
Chapter 9 9.1
9.2
9.3
Sessions 205
Authentication 221
Auth core 221 9.1.1 Authentication 223 9.1.2 Strategy 227 9.1.3 Sessions 231 9.1.4 Errors 232 9.1.5 Responses 233 9.1.6 Helpers 234 9.1.7 Router helper 235 9.1.8 Customizations 236 9.1.9 Callbacks 236 Auth more 237 9.2.1 Strategies 238 9.2.1.1 Basic 239 9.2.1.2 Password form 241 9.2.1.3 OpenID 242 9.2.2 Models 242 9.2.3 Controller 245 Auth password slices 245 9.3.1 Lib file 246
From the Library of Shirong Chen
xvi
Contents
9.4
9.3.2 Controller 247 9.3.3 Views 249 Conclusion 251
Chapter 10 10.1
10.2 10.3
10.4 10.5 10.6
Configuration 253 10.1.1 SMTP 253 10.1.2 Sendmail 254 10.1.3 Test send 254 10.1.4 Custom delivery methods Using mailers directly 255 Mail controllers 257 10.3.1 Invoking actions 258 10.3.2 Parameters 259 10.3.3 Attaching files 260 10.3.4 Templates 261 Testing 261 Generation 261 Conclusion 262
Chapter 11 11.1 11.2 11.3 11.4
Mailers 253
254
Parts 263
Parts controllers 263 Invoking actions 266 Generation 267 Conclusion 267
Chapter 12
Caching 269
12.1
Configuration 269 12.1.1 Fundamental stores 269 12.1.2 Strategy stores 270 12.2 Caching basics 271 12.2.1 Writing 271 12.2.2 Reading 272 12.2.3 Fetching 273 12.2.4 Deleting 273 12.3 Caching helpers 274
From the Library of Shirong Chen
Contents
xvii
12.3.1 Action caching 274 12.3.2 Eager caching 275 12.3.3 Fragment caching 277 12.3.4 Partial caching 277 12.4 Conclusion 278
Chapter 13 13.1 13.2 13.3 13.4 13.5 13.6 13.7 13.8 13.9
Testing 279
Rake tasks 279 Spec files 282 Model specs 283 Request specs 285 Request helper 289 Request matchers 290 RSpec extensions 292 Miscellaneous extensions Conclusion 295
Afterword Index
294
297
299
From the Library of Shirong Chen
This page intentionally left blank
From the Library of Shirong Chen
Foreword
I viewed the Merb project with a big dollop of suspicion almost from the very start. First of all, it started out as a really simple concept, but got a lot more complicated as additional contributors got involved. And whether my suspicion was misplaced or not, I couldn’t shake the feeling that the Merb people were insisting on reinventing Ruby on Rails simply to be difficult. Merb isn’t that different from Rails, nor is it a major improvement as far as I was concerned. It was only slightly better (in some regards), and only slightly different from Ruby on Rails. Sure there were some benchmarks showing big performance gains for using Merb, but I was not convinced enough to switch. And I couldn’t bear to use Merb and Rails concurrently. You see, they were very similar, yet different in subtle ways. The last thing I want to do when I’m programming on hard deadlines is to slow down to remember the differences between Merb’s render method and the one in Rails, etc. Life continued, and while I was very happy and making a ton of money working with and writing about Rails, I still occasionally looked at Merb, if only to see what they were up to. Sure enough, by 2008 my perception of Merb had started to change. I met Foy Savas and he gave me the gist of why it would make sense to write The Merb Way. I concurred and Foy got busy writing. As Merb approached a 1.0 release, it appeared that Yehuda Katz and his band of merry discontents were actually achieving a viable platform, one that would attain permanent status as an alternative to Rails. I started hearing about significant production projects running on Merb, such as yellowpages.com. My appreciation for Merb grew, as I got familiar with the project goals and noticed how the competition was acting as a healthy stimulus for Rails to not rest on its laurels. I wasn’t ready to switch to Merb myself, since my key criterion is maturity, which Rails has in spades, but no longer was I suspicious. xix From the Library of Shirong Chen
xx
Foreword
In November of 2008 I was extremely happy about Foy being almost finished writing the first manuscript of this book, knowing that it would be a good complement to the Professional Ruby Series and a good companion for The Rails Way. Foy delivered an awesome conference presentation in Boston where he covered some of the key concepts from the book in an engaging and entertaining manner—the crowd ate it up and I knew The Merb Way would be a winner. It wasn’t too many weeks later that the unthinkable happened. After an eruption of feuding between members of the Rails and Merb teams, some secret meetings occurred and shocking news was unveiled right before Christmas: DHH and Yehuda happily proclaimed that the core teams of both frameworks were merging and that within a year, the Merb codebase would be merged with Rails in order to produce Rails Version 3, after which development of Merb would discontinue. The million-dollar question (okay, not quite that much) for Foy and myself as the Series Editor was whether to continue with the publication of The Merb Way. After all, if the Merb framework was going away, then what was the point? After letting the matter settle for a while, we arrived at our answer, which you already know since you are holding this book. It turns out that learning about Merb is valuable in a number of important ways. First, Merb is still used rather widely and we suspect that its lifetime will exceed what the current core team has in mind. Second, Foy is a gifted writer and his descriptions of the philosophies that impacted the design and implementation of Merb are definitely useful to everyone who will be affected by the changes that will happen in Rails 3. It’s with great pleasure that I welcome the incomparable Foy Savas and publish The Merb Way as a full-fledged and proud installment of the Professional Ruby Series. I sincerely hope you get as much out of this book as I have. Obie Fernandez March 31, 2009
From the Library of Shirong Chen
Acknowledgments
First off, thanks go to all the Merb and DataMapper contributors. Obviously, no Merb would mean no Merb Way, but that’s certainly not all I want to thank them for. The elegance of both Merb and DataMapper, especially within their implementations, profoundly sets off my passion for holding code to an even higher standard of clarity. Consequently, throughout this book I have highlighted and included extensive amounts of Merb and DataMapper source code, truly making the contributors to both projects contributors to this book as well. I would like to thank those individuals with whom I have personally interacted the most: Matt Aimonetti, Ben Burkert, Dirkjan Bussink, Lori Holden, Michael D. Ivey, Michael Klishin, Dan Kubb, Carl Lerche, Daniel Neighman, Sam Smoot, Ezra Zygmuntowics, and especially Yehuda Katz. Completing this book against a backdrop of changing technology demanded the help of numerous friends and editors. Without their advice, analysis, and sharp eyes, I don’t know how it would ever have gotten done. To Sophia Chou, Andrew Guay, Kieran Huggins, Matt Knox, Jennifer Lindner, Bryan Ray, and Michael Thurston—thank you for each and every note and correction you made. A huge thank you goes to both Obie Fernandez and Debra Williams Cauley. You two are truly the driving force behind the Addison-Wesley Professional Ruby Series and the reason why it produces such high-quality books. Obie, had you not spotted the importance of Merb at such an early stage, inspired me with your own book on Rails, and pushed me to write one on Merb even after the frameworks merged, who knows if such a topic would have received the treatment it deserved? And Debra, simply wow. I know coordinating this book was no easy job, but you did so with such a seemingly paradoxical mix of patience and drive that I will remain thankful forever.
xxi From the Library of Shirong Chen
xxii
Acknowledgments
Finally, my warmest thanks and love go to all the members of my family who undeniably play a strong part in all that I do. Without your support, encouragement, perspective, and insight, I don’t know where, or more important who, I would be—thank you, all.
From the Library of Shirong Chen
Merb pastie
Anything that starts off with pure code is good in my book. Pun intended, here is Ezra Zygmuntowicz’s original Merb pastie, as found at http://pastie.org/14416, here formatted for the pages of this book. #!/usr/local/bin/ruby require 'rubygems' require 'mongrel' # Add the request handler directory to the load path. # Files in the 'app/controllers' dir will be mapped # against the first segment of a URL $LOAD_PATH.unshift( File.join( File.dirname( _ _FILE_ _ ) , 'app/controllers' ) ) PORT = 4000 # If true, controller source files are 'load'ed # rather than 'require'd so you can tweak code and # reload a page. ALLOW_RELOADING = true class String def import ALLOW_RELOADING ? load( self + '.rb' ) : require( end
self )
def controller_class_name self.capitalize
xxiii From the Library of Shirong Chen
xxiv
Merb pastie
end end class MerberHandler < Mongrel::HttpHandler def instantiate_controller(controller_name) controller_name.import begin return Object.const_get( controller_name.controller_class_name ).new rescue Exception # If life is sad, then print the error and # re-raise the exception: warn "Error getting instance of "+ "'#{controller_name.controller_class_name}':"+ "#{$!}" raise $! end end # Grab the request URL and break it up to get the # parts that map to the code request. There's a # simple assumption that the first part defines a # class holding the desired code. def handle(request) path = request.params["PATH_INFO"] puts request.inspect puts '='*50 # Might want to consider returning a # default object if we have a bare URL. return [nil, nil, nil ] if path =˜ /ˆ\/$/ c, m, args = path.to_s.gsub( /ˆ\//, '' ).split( '/' , 3) args = args.to_s. strip.empty? ? nil : args.split( '/' ) # Return an array with our object instance, # the method name, and any args. [ instantiate_controller(c), m, args ] end def process(request, response) response.start(200) do |head,out| head["Content-Type"] = "text/html" begin # Looks at the URL and breaks it up into # chunks that map to a class, a method call, # and arguments.
From the Library of Shirong Chen
Merb pastie
xxv
# Basically, # /foo/bar/baz/baz # ends up becoming # Foo.new.bar( baz, baz ) controller, method, args = handle(request) if controller # No allowance for default methods. # Worth considering, maybe default # to 'index' or 'to_s' out [Merb.root / "public", nil] } c[:session_store] = 'none' c[:exception_details] = true c[:log_level] = :debug # or error, warn, info or fatal c[:log_stream] = STDOUT # or use file for logging: # c[:log_file] = Merb.root / "log" / "merb.log" c[:reload_classes] = true c[:reload_templates] = true } Merb::Router.prepare do match('/').to( :controller => 'my_very_flat_app', :action =>'index' ) end class MyVeryFlatApp < Merb::Controller def index "hi" end end
Stepping into its directory is not enough to run a very flat application. The comment on the first line, however, points us in the right direction. Using the -I flag, we can specify the init file for our Merb application: merb -I my_very_flat_app.rb
Now if you visit http://localhost:4000, you should see the message returned by the value of the index action.
From the Library of Shirong Chen
1.2
The layout of a Merb application
5
Design decision: the Principle of Least Surprise This simple example exhibits one of Merb’s founding principles, that of ‘‘least surprise.’’ Here, in order to give us the least surprise, any request handled by this method receives a response containing the returned string. This may seem like an obvious design decision, but other frameworks do not behave this way; among other things they presuppose that the purpose of your controller is nearly always to render a template. Consequently, they attempt to render that template without any such explicit method call. The Merb developers chose against so brashly deciding what your controllers would be used for, knowing that such a decision would come at the cost of hackability. They also realized that a great litmus test to make sure that the line between clear, hackable behavior and scope-narrowing magic remains uncrossed was the veritable Principle of Least Surprise, also known as the Principle of Least Astonishment. Anyway, don’t be surprised (pun intended) if you see signs of this principle at work in other aspects of Merb.
Getting back to the code, there are three sections: one for configuration, another for routing, and finally a controller. Each of these sections is fundamental in Merb application development, and that’s why they’re there. Strictly speaking, though, the configuration section wasn’t necessary in order to send “hi” to your browser. If it wasn’t there, Merb would use its default values instead. Design decision: Convention over Configuration This, too, is another example of a principle to which Merb adheres, Convention over Configuration. Ruby on Rails notably popularized this paradigm, and Merb continues the tradition. We’ll find, however, that Merb has tempered the concept of Convention over Configuration through the primacy of hackability. To be more concrete, if a default configuration lessens the amount of code needed without reducing the hackability of your application, then Merb has provided it for you. This demonstrates how Merb has striven to keep opinions at bay and give you the maximum power to get the job done, no matter what the job.
Let’s take a look at the specifics of the config block and see which of the defaults have been overridden. The first of these is the location of public static assets. If you create a directory public/ in the same directory as the application and then add an image, say merb.jpg, you’ll be able to view it at http://localhost:4000/merb.jpg. The second line relating to sessions doesn’t actually override any default but has been included for you to change at will. The third line in the config block switches exception
From the Library of Shirong Chen
6
Chapter 1: Fundamentals
details to true. This allows you to see full exception details on error pages when they occur. For an example, take a look at http://localhost:4000/mybad. With exception details set to true, you will be able to view a stacktrace right in the browser. Set the config value instead to false, and after restarting Merb, you’ll no longer have access to the stacktrace via the browser.
1.2.2 Flat layout If you want to use view templates, but still prefer a single file for the bulk of your code, a flat layout may be just what you’re looking for. The flat layout is optimal for simple Merb applications that do not use models but still have templates. Using merb-gen once again, you can create a flat app using the --flat option: $ merb-gen flat my_flat_app Generating with flat generator: [ADDED] Rakefile [ADDED] application.rb [ADDED] config/framework.rb [ADDED] config/init.rb [ADDED] views/foo.html.erb [ADDED] spec [ADDED] gems [ADDED] merb.thor [ADDED] .gitignore [ADDED] README.txt
This creates a directory structure as follows: . |-|-|-|-| | |-|-|-| ‘-|
README.txt Rakefile application.rb config |-- framework.rb ‘-- init.rb gems merb.thor spec ‘-- spec_helper.rb views ‘-- foo.html.erb
In order to run a flat layout, all we have to do is step into its directory and run merb. You’ll notice that we no longer need to specify a file to include. This is because by default config/init.rb is automatically included.
From the Library of Shirong Chen
1.2
The layout of a Merb application
7
Incidentally, one of the largest benefits of jumping from the very flat to the flat layout is that the presence of the init file allows us to avoid restarting Merb every time we make changes to application code. Instead, in development mode, Merb automatically checks to see if application files have been updated and reloads the appropriate classes for us. You can try this by running Merb and altering the application.rb index method to say "Something Different" instead of "Hello...". If you check via your browser, the response will have changed without the need to restart Merb. Other environments Note that by convention, class reloading applies only to Merb applications running in development mode. This is the default mode for a Merb application, and it’s what we’ve used in every example so far. There are two other standard environments for running Merb applications---production and testing---and we’ll take a look at those later on.
The principal difference here from the Merb standard layout is that all controller code (and possibly model code) is found in application.rb. Another difference is the inclusion of config/framework.rb. This file is actually what prevents Merb from thinking the application is of the default form by defining the flat layout. You’ll notice that config/framework.rb does this by just defining a hash: Merb::Config[:framework] = { :application => Merb.root / "application.rb", :config => [Merb.root / "config", nil], :public => [Merb.root / "public", nil], :view => Merb.root / "views" }
If you change these values, Merb will on boot expect to find particular code in different places. Let’s say you start with a flat application, but soon decide it needs to grow to the standard layout. You won’t have much trouble accomplishing the transition. Here is a series of terminal commands to help you on your way: $ $ $ $ $
mkdir app mv views app/ mkdir app/controllers mv application.rb app/controllers/ rm config/framework.rb
From the Library of Shirong Chen
8
Chapter 1: Fundamentals
As exhibited above, it’s the removal of the config/framework.rb file that defaults us to the standard layout. Finally, though this will run as an application in standard layout, there may be a few more items of cleanup you’d like to attend to. Here’s a list to keep in mind: • Pull out the router in config/init.rb and put it in config/router.rb. • Separate app/controllers/application.rb into as many controllers as needed. • Alter your controllers to subclass from Application instead of Merb:: Controller.
1.2.3 Standard layout Now let’s take a look at the standard layout. Returning to the directory of the first application that we created with merb-gen, we find the following directory tree: . |-|-| | | | |-|-| | | | | | |-|-|-| | |-| | | | | | |-‘--
Rakefile app |-- controllers |-- helpers |-- models ‘-- views autotest config |-- database.yml |-- dependencies.rb |-- environments |-- init.rb |-- rack.rb ‘-- router.rb doc gems merb |-- merb-auth ‘-- session public |-- favicon.ico |-- images |-- javascripts |-- merb.fcgi |-- robots.txt ‘-- stylesheets spec tasks
From the Library of Shirong Chen
1.3
Interactive Merb
9
We haven’t included all the files the standard layout creates (there are a lot of them), but just enough to understand the structure. The directories spec/ and autotest/ serve to facilitate the testing of your application. The Rakefile and tasks directories provide common tasks for use in your application. For now, though, what matters most are these three directories: app/, config/, and public/. The first of these stores all of your application code. To keep everything organized in an MVC way, app/ also has a few subdirectories of its own. By default these include controllers/, helpers/, models/, and views/. Next up, config/ is a directory we saw in the flat layout. It holds init.rb and a number of other environment configuration files. Unlike the flat layout, however, it has a dependencies.rb file which is referenced in init.rb and by default includes the dependencies making up the Merb stack. Another file in the config directory is router.rb. This defines the routes for your application, mapping requests to specific controller actions. Another file definitely worth noticing is database.yml, which defines database access on a per-environment level. Finally, the directory public/, which we also saw in the flatter layouts, is meant to hold assets for your application to serve. These may include images, stylesheets, javascripts, and other static files.
1.3 Interactive Merb Often you’ll want to be able to dig around in a running Merb application. Interactive Merb allows you to do this by giving you an IRB session on a running Merb application. There are two ways to get this kind of console access. The first and simpler way is to add the -i flag to the command merb: $ merb -i Loading init file from /home/foysavas/src/first_app/config/init.rb Loading /home/foysavas/src/first_app/config/environments/ development.rb ˜ Connecting to database... ˜ Loaded slice 'MerbAuthSlicePassword' ... ˜ Parent pid: 25514 ˜ Compiling routes... ˜ Activating slice 'MerbAuthSlicePassword' ... irb: warn: can't alias context from irb_context. irb(main):001:0>
This creates an exclusive interactive instance of your Merb application not bound to any port. If, however, you wanted a Merb application that could respond to web requests as well, then using the -C flag provides a console trap on an otherwise typical
From the Library of Shirong Chen
10
Chapter 1: Fundamentals
Merb instance. To get to this console you have to hit Ctrl-C once Merb is booted. We do this below and then immediately return to server-only mode with the command exit. foysavas@navi:˜/src/first_app$ merb -C Loading init file from /home/foysavas/src/first_app/config/init.rb Loading /home/foysavas/src/first_app/config/environments/ development.rb ˜ Connecting to database... ˜ Loaded slice 'MerbAuthSlicePassword' ... ˜ Parent pid: 26366 ˜ Compiling routes... ˜ Activating slice 'MerbAuthSlicePassword' ... merb : worker (port 4000) ˜ Starting Mongrel at port 4000 merb : worker (port 4000) ˜ Successfully bound to port 4000 ˆCmerb : worker (port 4000) ˜ Interrupt a second time to quit. irb(main):001:0> exit merb : worker (port 4000) ˜ Exiting from IRB mode back into server mode.
Using interactive Merb during development can often speed up the process since you’ll be able to directly interact with the code you’re writing. Some words of caution, though. First, don’t let this lead to writing complex methods you can’t comprehend, just because they produce the right results. Second, direct interaction can’t take the place of proper behavior testing, so don’t treat it that way. Making either of these mistakes will only put you behind later on, especially when you need to rework or extend what you have. Another practical situation when interactive Merb is helpful is after you’ve deployed a production application and need to determine why something has seemingly gone wrong and correct it. Whether the bug is the result of a subtle misstep in deployment or an incidental result of rare production data, you’ll be glad you weren’t without console interaction.
1.3.1 Console methods Interactive Merb also comes with a number of console methods, all of which are accessible through the object merb. These let you do things such as conveniently faking requests within interactive Merb. Below we use a helper method to retrieve a response body from a URL using the HTTP GET method on a fresh very flat application. > merb.get('/').body ˜ Params: {"action"=>"index", "controller"=>"my_very_flat_app"} ˜ {:after_filters_time=>1.6e-05, :before_filters_time=>2.0e-05, :action_time=>0.000229} => "hi"
Table 1.2 lists the methods available on the merb object along with their use.
From the Library of Shirong Chen
1.3
Interactive Merb
11
Table 1.2 merb console methods Method
Use
check_request_for_route
Returns the parameters of what route a request matches
close_sandbox!
Ends a sandboxed session
delete
Uses HTTP DELETE on a URL with optional parameters
dispatch_request
Dispatches a request mimicking its handling and the return of a controller
dispatch_to
Dispatches to a specific controller action, with optional parameters
fake_request
Returns a request composed of defaults and merged-in parameters
get
Uses HTTP GET to retrieve a URL
open_sandbox!
Starts a sandboxed session
post
Uses HTTP POST on a URL with optional parameters
put
Uses HTTP PUT on a URL with optional parameters
reload!
Reloads your Merb application code
request
Builds a request from a URL and the request environment
show_routes
Lists all compiled routes in a tabular fashion
trace_log!
Explicitly sets logger output to be flushed
url
Generates a URL given parameters
1.3.2 Sandboxing Interactive Merb is capable of sandboxing your activity, allowing you to mess around with database resources only to have all changes reverted when you exit the sandbox. This is extremely useful when you have fixture data (and for those who live dangerously, even production data) that you need to work with. Because we haven’t seen
From the Library of Shirong Chen
12
Chapter 1: Fundamentals
anything that involves databases yet, we’ll leave the following example for you to revisit at will: > User.count ˜ SELECT COUNT(*) FROM ‘users‘ => 1 > merb.open_sandbox! Loading development environment in sandbox (Merb 1.0.0) Any modifications you make will be rolled back on exit => [Merb::Orms::DataMapper] > User.all.destroy! ˜ DELETE FROM ‘users‘ => true > User.count ˜ SELECT COUNT(*) FROM ‘users‘ => 0 > merb.close_sandbox! Modifications have been rolled back => nil > User.count ˜ SELECT COUNT(*) FROM ‘users‘ => 1
1.4 Merb configuration The config/ directory found in the root of a Merb application holds all the configuration files for your application. Some of these files, like init.rb, are always there. The others depend upon the demands of your application. Let’s take a look at the most typical configuration files that Merb files use and what’s inside each of them.
1.4.1 Init script The init script is the first thing that Merb runs. As we saw when moving up from the very flat to the flat layout, it provided us with a jump in agility during development through class reloads. The init script itself, though, serves many other purposes, including basic configuration, inclusion of dependencies, and the selection of a testing environment.
1.4.1.1 Gems and load path In the Merb world, gems are the preferred form of distributing plugins and libraries. Merb developers did not want to create yet another vendor lock-in-inducing plugin system. Instead, RubyGems was chosen since it’s already the standard for distributed packets of Ruby functionality and could work well with the Merb plugin infrastructure.
From the Library of Shirong Chen
1.4
Merb configuration
13
Doing things the Ruby way Merb developers strive to get things done the Ruby way, that is, in the way most idiomatically compatible with the Ruby language and community. The use of RubyGems to distribute plugins is just one example of this. Others, like the refusal to use Symbol#to_proc or Module#alias_method_chain, not only keep Merb understandable but give it performance advantages over Rails and other frameworks.
Additionally, the convenience of the gem-based system allows us to install specific gems within an application, in the directory gems/ in the application’s root directory. When the gems are loaded as dependencies, they are given preference over system-wide gems. The benefit of being able to install version-specific gems in your application is huge, allowing you to • Share your application code with other developers without worrying about having
different versions • Deploy your application without any need to install all the gem dependencies on
the production server If at some point you want to freeze a specific gem inside your Merb application, you can do so using the install directory -i flag with gem. Here we do this from inside a directory with a gem by issuing the following command: $ gem -i ˜/src/your_app/gems dm-geokit-1.0.0.gem
Note that gem also installs the gem’s listed dependencies into your application for you. You may want to pass in the flag --no-rdoc to save on the size of your application’s gem/ directory.
1.4.1.2 Dependencies Including gems as dependencies instead of directly requiring has been done primarily so that we can load application-installed gems if they exist. The dependency method also defers loading until after logging has been initiated. With this in mind, you should be able to use the method dependency just as if you were using require. If the string you pass in doesn’t refer to a gem, dependency will attempt to load it as a file via require. dependency 'dm-geokit' dependency 'geokit-core', '>= 0.9.0' dependency 'lib/support/string'
From the Library of Shirong Chen
14
Chapter 1: Fundamentals
You can also use the method dependencies to require a few dependencies at once instead of one per line. By passing in hashes, you specify gem version. dependencies "RedCloth", "merb_helpers" dependencies "RedCloth" => "3.0"
1.4.1.3 after_app_loads Inside init.rb, you’ll find a method taking a block called Merb::BootLoader.after_ app_loads. The boot loader evaluates this passed-in block after most of the application is loaded. This is useful if you need certain classes loaded before including a dependency or defining a constant within a module: dependency 'merb_admin' GeoKit::default_units = :miles
1.4.1.4 Template engine You have your choice of template engines when using Merb. In fact, you can use multiple template engines by embedding templates inside one another. Still, when it comes to the template merb-gen, the templates are generated for ERB. If you prefer that they be in Haml when possible, you can change the following lines in init.rb so that the method use_template_engine has the :haml parameter: use_template_engine :erb # use_template_engine :haml
1.4.1.5 Basic configuration Merb stores configuration settings inside the class Merb::Config. You can do this in two different ways, first by explicitly using the class methods Merb::Config.[] and Merb::Config.[]=: Merb::Config[:session_store] = 'memory' if Merb::Config[:verbose] Merb.logger.info "Above all be terse." end
or instead by passing a block into the method Merb::Config.use: Merb::Config.use do |c| c[:session_store] = 'cookie' end
1.4.1.6 ORM options Inside the init script we see the lines for the three most popular ORMs, or Object Relations Mapping libraries, that work with Merb. Enabling any one of them is as easy as uncommenting a line. Below we’ve enabled the DataMapper ORM.
From the Library of Shirong Chen
1.4
Merb configuration
15
# Uncomment for DataMapper ORM use_orm :datamapper # Uncomment for ActiveRecord ORM # use_orm :activerecord # Uncomment for Sequel ORM # use_orm :sequel
For more information on ORMs, please see Chapter 5 on models, which uses DataMapper and briefly covers the use of both ActiveRecord and Sequel.
1.4.1.7 Testing options Another thing the init.rb file handles is your choice of testing suite. By default, Merb uses RSpec, and we’ll go over how to use it later on. If, however, you want to use Test::Unit instead, that’s as easy as commenting out one line and uncommenting the other: # use_test :test_unit use_test :rspec
Note that to complete this switch you may also have to install the merb_test_unit gem if it hasn’t been installed already.
1.4.1.8 Inflector At the bottom of the Merb init script you’ll find details about how to customize the inflector. The inflector is used to take English words (really just nouns) and switch between their singular and plural forms. This may seem odd to include in an MVC framework, but its existence allows us a lot of ease in abstracting URLs and working with RESTful resources (REST stands for Representational State Transfer). That said, you may not end up using the inflector, but in case you do, it’s always there. You can test it in interactive Merb, simply by tacking the methods singular or plural onto strings of nouns: > "users".singular => "user" > "forum".plural => "forums"
If, however, you run into a word that is not being pluralized as you need it to be (let’s say, for example, you wanted the plural of forum to be fora), you would be able to use the init.rb file to assure this behavior by adding a line like this: Extlib::Inflection.word "forum", "fora"
From the Library of Shirong Chen
16
Chapter 1: Fundamentals
1.4.2 Environments Merb can standardly run in three different environments: development, testing, and production. Up until now we’ve run Merb only under development mode. You can run Merb in these other modes by specifying the environment after the -e flag with the merb command: $ merb -e production
Each of these modes runs slightly differently. These differences are defined in the files inside config/environments. For instance, inside config/environment/ development.rb, we find Merb.logger.info("Loaded DEVELOPMENT Environment...") Merb::Config.use { |c| c[:exception_details] = true c[:reload_templates] = true c[:reload_classes] = true c[:reload_time] = 0.5 c[:ignore_tampered_cookies] = true c[:log_auto_flush ] = true c[:log_level] = :debug c[:log_stream] = STDOUT c[:log_file] = nil # Or redirect logging into a file: # c[:log_file] = Merb.root / "log" / "development.log" }
This file uses the method Merb::Config.use, just as init.rb does, to set environmentspecific settings. You can also set other constants or whatever environment-specific variables you need in here. We’ve seen some of these configuration settings before. The settings reload_classes and reload_time define what a development environment is by setting application classes to reload periodically. The low log level as well as log auto-flushing also mean that in development mode your application will log everything and write it out as it comes. Let’s take a look at production.rb to make a comparison: Merb.logger.info("Loaded PRODUCTION Environment...") Merb::Config.use { |c| c[:exception_details] = false c[:reload_classes] = false c[:log_level] = :error
From the Library of Shirong Chen
1.4
Merb configuration
17
c[:log_file] = Merb.root / "log" / "production.log" # or redirect logger using IO handle # c[:log_stream] = STDOUT }
Note that exception details are off, and users of the site will not see backtraces if they run into an error. The reloading of classes is also off to save on performance. Finally, the log level has been pushed up to error, because we don’t want to be dealing with huge logs. The log file has been explicitly set, and by omission, log auto-flushing is off. The third environment configuration file, test.rb, is used only when running tests. Its environment configuration is decently empty, setting a configuration key :testing to true. Note that you are able to create your own environments as well as configuration files. To do so, just pass in the same name to the -e flag as the filename of your custom configuration. Here we check via interactive Merb that we’re running in the “staging” environment: $ merb -i -e staging > Merb.environment => "staging"
1.4.3 Router The Merb file config/router.rb defines the routes used by your application. We’ll cover this in detail in the next chapter, but if you need only the most basic setup, you can rely on the default routes created by the line default_routes
1.4.4 Logging By default, all Merb logs are stored in log/ in the Merb root directory. If you’re running Merb under a testing environment, then the log file will be named merb_test.log. Otherwise, it’ll be labeled based on the port number under which it’s running. Typically this means the log filename will be merb.4000.log. You can manually change the location of the log file by setting Merb::Config[:log_file]. This is typically done per the environment and by default results in the log production.log for a Merb application running in production mode. The other log settings that can also be altered as part of Merb::Config are log_level and log_auto_flush. Log level determines
From the Library of Shirong Chen
18
Chapter 1: Fundamentals
the minimal level for a log message to be recorded. Starting from the lowest, these levels are • Debug—intended for development-only logging used for debugging • Info—typically used for logging uncommon events so you know when they hap-
pened • Warn—best used to warn developers that a near-miss happened though no error
occurred • Error—occurs when a user experiences an error • Fatal—logs an error that has killed the server
You can set Merb::Config[:log_level] by using a symbol for any one of these. log_auto_flush, on the other hand, can be set to true or false and will determine whether the log buffer is automatically flushed out to the log file or not.
1.4.4.1 Using logging Throughout your Merb application you will have access to logging via the Merb.logger. When the logger receives a call to any of the log levels, it logs the string it is given as a parameter. Here are some examples of logger usage: Merb.logger.info "Insides of @current_user:" Merb.logger.info @current_user.inspect
If auto-flushing is off, you can manually flush the logs by either using the method flush or using the bang method variant of a log level: Merb.logger.warn! "Need this in the log file now!" Merb.logger.warn "User creation has failed." Merb.logger.flush
1.4.4.2 Viewing logs Unless your Merb application is running as a background process, the first place you see your logs is in the terminal you run it under. If, however, you’re in production mode, you could use the UNIX command tail to actively watch your logs. Here we stream log changes to the terminal by using the flag -f: $ tail -f log/merb.4000.log
If more than one server is running, you can use a wildcard to stream all the logs. Below we also use the v flag to identify which file has been modified. $ tail -vf log/*.log
From the Library of Shirong Chen
1.4
Merb configuration
19
1.4.5 Database When you use an ORM with Merb, it has to know how to access the database or databases you use in the different environments. We’ll go over ORMs more in depth later on, but for reference let’s take a look at how the file config/database.yml helps DataMapper connect. As the YAML files are broken down into three sections, one for each environment, and since each of these is virtually the same, we’ll look at only one of them: development: \&defaults # These are the settings for repository :default adapter: postgres database: sample_development username: the_user password: secrets host: localhost # Add more repositories # repositories: # repo1: # adapter: postgres # database: sample_development # username: the_user # password: secrets # host: localhost # repo2: # ...
Here, adapter specifies the type of database program you’re running (and incidentally the adapter DataMapper will have to use to communicate with it). The rest of the lines are pretty self-explanatory. However, if you access your development database without a password (don’t ever do such a thing on a production server!), you may wonder how to do it. It’s simple enough; just leave whitespace after password: and before the next line. Two-space indents YAML files are whitespace-sensitive so you’ll have to make sure that there are two spaces for every nested line. Among Ruby developers, this is also the standard way of indenting code. Though it’s not necessary with Ruby code, please, for everyone’s sake, abide by this rule.
The extra repository options commented after the host line are a powerful option. DataMapper allows you to access alternative databases through either more explicit
From the Library of Shirong Chen
20
Chapter 1: Fundamentals
method calls or by specifying when they apply in model files. You can add these extra repositories under repositories by starting each with a name by which to reference it. Last, on the first line you may have noticed the inclusion of &defaults. This allows us to use these settings as defaults in the other environments. To do this, we’ll use "\\*\\?\\{\\}\\." "\\*\\?\\{\\}\\.".unescape_regexp #=> "*?{}."
• snake_case and camel_case—return a string in either snake or camel case. These
are often used to convert class names to filenames and back again: "FooBar".snake_case #=> "foo_bar" "foo_bar".camel_case #=> "FooBar"
• to_const_string and to_const_path—used to convert a constant name to a
standard path and back again: "merb/core_ext/string".to_const_string #=> "Merb::CoreExt:: String" "FooBar::Baz".to_const_path # => "foo_bar/baz"
• /—used to join strings as part of a file path (uses File.join): "merb"/"core_ext" #=> "merb/core_ext"
• String.translate, String.translations, and t—enable phrase translations
with replacements: spanish_translations = { "hello %s" => "hola %s", "world" => "mundo" } spanish_translations.each_pair{ |x,y| String. translations[x] = y } String.translate("world") #=> "mundo" "hello %s".t("merb") #=> "hola merb" "hello %s".t("world") #=> "hola mundo"
From the Library of Shirong Chen
22
Chapter 1: Fundamentals
1.5.1.5 Time • to_json—returns an ISO-8601-compatible rendering of the Time object’s properties • to_datetime—returns the DateTime equivalent
1.5.1.6 DateTime • to_time—returns the Time equivalent 1.5.1.7 Pathname • /—returns its receiver plus parameter as path fully expanded 1.5.1.8 blank? blank? is a method that has been added to numerous classes. In each of these, the blank? method may vary slightly. Here’s a list of the classes to which it’s been applied and what it tests: • Object—tests if the object is nil; if it is not, it tests first if it responds to empty?
and then whether it is empty or not • Numeric—always responds false • NilClass—always responds true • TrueClass—always responds false • FalseClass—always responds true • String—asks if the string less whitespace is empty?
1.5.1.9 Logger Extlib::Logger is the logger used by both Merb and DataMapper. The Merb log is specifically created in a boot loader we’ll see later on, but creating another logger for any of your Ruby applications is easy enough: logger = Extlib::Logger.new('/tmp/extliblog') logger.info "Log!!"
1.5.1.10 Mash Mash is a subclass of Hash designed specifically to allow you to interchangeably use strings or symbols as keys. With Merb, the real impetus for the creation of Mash was being able to refer to request parameters by both the string key they come with and the more Ruby-programmer-friendly symbolic key.
From the Library of Shirong Chen
1.5
Understanding the Merb stack
23
1.5.1.11 SimpleSet SimpleSet is a simulation of a set where each object is a key in a Hash. This class is used to manage callable actions in Merb controllers. 1.5.1.12 VirtualFile VirtualFile is really just StringIO with a path accessor. This allows Merb to use strings as inline templates. 1.5.1.13 LazyArray LazyArray is an Enumerable used by DataMapper as the parent class of DataMapper::Collection. Notably, LazyArray is designed to limit the loading of data sets to only when they are needed. As an application developer, you won’t need to know any of its methods directly, but knowing of their existence is definitely useful in understanding DataMapper’s beauty. 1.5.1.14 Hook Extlib::Hook is a module used in DataMapper that allows models to associate methods before or after the execution of other methods. It’s similar in use and style to the before and after filters we’ll find in Merb controllers but is built quite differently. 1.5.1.15 Pooling Extlib::Pooling is a module that allows for the sharing of similar resources with the same purpose. It is used by DataMapper to pool database connections.
1.5.2 Rack Rack is a Ruby web server interface that shuttles responses from web applications to actual web servers. The simplicity, modularity, and adaptability of Rack’s implementation are truly beautiful. Consequently, it is used not only by Merb but by numerous other Ruby web frameworks. These frameworks not only have the convenience of allowing programming to an interface but have also enjoyed the benefit of sharing the code that finally pushes responses to different web servers.
1.5.2.1 Adapter options Rack leaves the final decision of which web server to use up to the application developer. The default web server is Mongrel, but you can change this option by passing in an option to Merb on the command line after the -a or --adapter flag or by setting the
From the Library of Shirong Chen
24
Chapter 1: Fundamentals
config variable Merb::Config[:adapter]. Table 1.3 is a list of the adapters available through Rack and the Merb core. If you feel overwhelmed by the number of web server options, don’t worry: The default Mongrel server will most likely serve all your needs, at least at first.
1.5.2.2 How Rack works Rack works by passing around a Ruby object that responds to the call method. This call method needs to take one block parameter known as the environment and return an array of three values: status, headers, and body. This design choice makes Rack applications capable of being as small as lambda expressions, which intrinsically accept a call method. Here’s a “Hello World” example using just Rack and Mongrel: require 'rack' app = lamdba{[ 200, {"Content-Type" => "text/plain"}, ["Hello World"] ]} Rack::Handler::Mongrel.run(app, {:Host => "127.0.0.1", :Port => 4000})
Merb, however, uses Rack in a more complex way than this. Here is the abbreviated source from the Merb core that defines the Merb Rack application: class Merb::Rack::Application def call(env) begin controller = ::Merb::Dispatcher.handle(Merb::Request.new(env)) rescue Object => e return [500, {Merb::Const::CONTENT_TYPE => "text/html"}, e.message + "
" + e.backtrace.join("
")] end Merb.logger.info "\n\n" Merb.logger.flush controller.rack_response end end
You can see how the call method uses the passed-in Rack environment, which is just the incoming request from the web server, and then ultimately returns a response
From the Library of Shirong Chen
1.5
Understanding the Merb stack
25
Table 1.3 Available adapters Adapter
Key
Description
Mongrel
(default)
Mongrel is a web server created by Zed Shaw that boasts high performance.
Evented Mongrel
emongrel
Evented Mongrel is an enhancement of the Mongrel web server that handles concurrent requests with more consistent output by using EventMachine, a current IO library.
Swiftiplied Mongrel
swift
Swiftiplied Mongrel further enhances Evented Mongrel with an unconventional clustering proxy that eliminates significant overhead performance costs in setting up and tearing down connections.
Thin
thin
Thin is a web server that glues together the Mongrel server, EventMachine, and a builtin Rack interface.
Ebb
ebb
Ebb is a Ruby web server that uses the lightweight HTTP server library libebb that boasts performance similar to or better than that of Evented servers.
FastCGI
fcgi
FastCGI is another web interface that will plug into other web servers, including Abyss, Apache, Cherokee, LiteSpeed, Microsoft IIS, nginx, and the Sun Java System Web Server.
WEBrick
webrick
WEBrick is an older Ruby web server often used before Mongrel.
IRB
irb
Not actually a web server, the IRB adapter enables you to interactively run a Merb application.
Runner
runner
Also not an actual web server, but the Runner adapter is used to run tests.
From the Library of Shirong Chen
26
Chapter 1: Fundamentals
that Rack passes up to the web server for client distribution. Here are the steps it takes: 1. Create a request object using the Rack environment which contains everything from the incoming request. 2. Use the dispatcher to handle the newly created request object, resulting in a controller object. 3. Rescue any unhandled errors, returning a backtrace if needed. 4. Flush the log. 5. Return the Rack response crafted by the controller.
1.5.3 ORMs If your applicaton needs to store, retrieve, and manipulate data in complex ways, using a database is a no-brainer. But having to litter your code with SQL queries is painful. Fortunately, a number of ORMs enable you to interact with a database almost without ever having to write a SQL statement. In this book we’ll mostly focus on the DataMapper ORM, which is part of the standard Merb stack. Still, because there are options, the Merb core stays agnostic, and when it comes down to picking the ORM, it’s all up to you. Here are the three officially supported ORMs: • ActiveRecord is the Rails ORM. If you’re porting a Rails application to Merb, using
the ActiveRecord ORM will save you the trouble of having to modify your models. • DataMapper is a next-generation ActiveRecord-like ORM that innovates past its
predecessor. Some of its enhancements include identity maps, lazy loading, strategic eager loading, dirty property tracking, connection pooling, and a Ruby syntax that minimizes or eliminates the need to ever write SQL statements. • Sequel is a robust ORM offering thread safety, connection pooling, and a very
natural DSL for constructing database queries and table schemas. It has numerous well-supported adapters as well as a sophisticated feature set, including prepared statements, bound variables, master/slave configuration, and database sharding.
1.5.4 Plugins The Merb core stays away from dumping in everything you aren’t going to use just in case you’ll need it in another project. Instead, Merb emphasizes plugins as integral parts of the Merb stack. This keeps your application footprint small, but it also demands knowledge of what plugins exist. The most often used plugins are maintained by the
From the Library of Shirong Chen
1.6
An overview of Merb internals
27
Merb developers themselves and are included in the standard Merb stack. Less often used plugins are distributed by third parties.
1.6 An overview of Merb internals You don’t need to understand Merb internals for the work you’ll do as an application developer, but taking a look under the hood to understand how it all came together will help you use them better.
1.6.1 Boot loaders When Merb starts up, the first significant thing it does is run its boot loader. This makes it the best place to start understanding Merb internals. More important, casual knowledge of the startup routine of the Merb boot loader can change your perception of Merb from that of a black box that runs your application code to a framework you can pull apart and extend. Merb actually uses subclasses of the principal boot loader to define subroutines needed at boot-up. The class Merb::BootLoader thus serves two major functions. First, it is the parent class of all boot loaders. Second, at startup it goes through each of its subclasses and calls a class method named run. With this in mind, let’s take a look at each of the subclassed boot loaders in the order in which they are loaded. We’ll also include excerpts from their run methods. There’s no need to be overwhelmed by the code included (especially since externally defined variables appear). Instead, just let it simmer, and appreciate the fact that you can concretely trace things back to their source.
1.6.1.1 BootLoader::Logger The first boot loader to be loaded is the logger. We need it to come first so that we can log any issues that may occur during boot-up. Here’s the code from inside the run method of the logger boot loader: Merb.logger = Merb::Logger.new(Merb.log_file, Merb:: Config[:log_level], Merb::Config[:log_delimiter], Merb::Config[:log_auto_flush])
Note how the Merb::Config variables we saw earlier are used to initiate the logger. Merb.log_file may seem unfamiliar, but it’s really only a method that references Merb::Config['log_file'] or otherwise constructs a default log filename.
From the Library of Shirong Chen
28
Chapter 1: Fundamentals
1.6.1.2 BootLoader::DropPidFile When our application is run either as a daemon or as one server among a cluster, we’ll need to store its process ID, so that we can, among other things, kill it later using merb -k. The DropPidFile boot loader does this for us by storing our server’s process ID number in a file found in log/: Merb::Server.store_pid(Merb::Config[:port]) if Merb:: Config[:daemonize] || Merb::Config[:cluster]
This run method doesn’t reveal much of what goes on, but by default the pidfile is named based on the number of the port on which the server is running. So, for example, the standard pidfile can be found at log/merb.4000.pid. You can change this default by setting Merb::Config[:pid_file] directly within either your config file or an environment file.
1.6.1.3 BootLoader::Defaults Within the Merb default boot loader, some critical overrides are defined to make up for the inability of a number of browsers to use all the HTTP methods, particularly PUT and DELETE. This is particularly useful in building RESTful routes for resources. Note how the default boot loader overrides some requests to imitate the client being capable of all the HTTP methods: Merb::Request.http_method_overrides.concat([ proc { |c| c.params[:_method] }, proc { |c| c.env['HTTP_X_HTTP_METHOD_OVERRIDE'] } ])
1.6.1.4 BootLoader::BuildFramework As we saw in the config section, Merb allows us to designate alternative layouts for our applications. The BuildFramework boot loader gets this working: if File.exists?(Merb.root / "config" / "framework.rb") require Merb.root / "config" / "framework" elsif File.exists?(Merb.root / "framework.rb") require Merb.root / "framework" else Merb::BootLoader.default_framework end (Merb::Config[:framework] || {}).each do |name, path| path = Array(path) Merb.push_path(name, path.first, path.length == 2 ? path[1] : "**/*.rb") end
From the Library of Shirong Chen
1.6
An overview of Merb internals
29
Note how, in the final lines, the inclusion of a second parameter alters the default of '**/*.rb'. This string is known as a glob and is used to recursively search for Ruby files. Changing the glob affects what files are included. This may come in handy in framework definition, where we want to include only one file (set the glob to nil) or only files named in a particular fashion (for example, '*_controller.rb').
1.6.1.5 BootLoader::Dependencies This boot loader actually does a bit more than load dependencies. It loads the init file, loads the applicable environment configuration file, loads other dependencies you’ve included in the init or environment file, and then finally updates the logger with any changes you may have made. 1.6.1.6 BootLoader::MixinSession This adds in session functionality by including relevant mixins in both Merb:: Controller and Merb::Request. It’s critical that this happens so early because custom subclasses of SessionContainer and SessionStoreContainer may be created through a before_app_loads block: def self.run require 'merb-core/dispatch/session' Merb::Controller.send(:include, ::Merb::SessionMixin) Merb::Request.send(:include, ::Merb::SessionMixin::RequestMixin) end
1.6.1.7 BootLoader::BeforeAppHooks This boot loader can be used to set or define whatever you need before your application code gets loaded. Plugins often use this hook to include code. The boot loader works by calling all the before load callbacks created by before_app_loads blocks: Merb::BootLoader.before_load_callbacks.each { |x| x.call }
1.6.1.8 BootLoader::LoadClasses The LoadClasses boot loader loads all the nontemplate application code. It also records modification times for each file so that they can be used by the ReloadClasses boot loader. Since the code gets fairly complex, we’ll leave it to you to check the source yourself. 1.6.1.9 BootLoader::Templates This boot loader does nearly the same thing as the LoadClasses boot loader but for templates. Pulling from a template path, it inlines each of the templates for later use by the controllers:
From the Library of Shirong Chen
30
Chapter 1: Fundamentals
template_paths.each do |path| Merb::Template.inline_template(File.open(path)) end
1.6.1.10 BootLoader::MimeTypes Merb is capable of rendering responses with different MIME types. The MimeTypes boot loader registers the default MIME types and serves as a good example of how you can add your own MIME types in an after_app_loads block: Merb.add_mime_type(:all, nil, %w[*/*]) Merb.add_mime_type(:yaml, :to_yaml, %w[application/x-yaml text/yaml], :charset => "utf-8") Merb.add_mime_type(:text, :to_text, %w[text/plain], : charset => "utf-8") Merb.add_mime_type(:html, :to_html, %w[text/html application/xhtml+xml application/html], :charset => " utf-8") Merb.add_mime_type(:xml, :to_xml, %w[application/xml text/xml application/x-xml], {:charset => "utf-8"}, 0.9998) Merb.add_mime_type(:js, :to_json, %w[text/javascript application/javascript application/x-javascript], : charset => "utf-8") Merb.add_mime_type(:json, :to_json, %w[application/json text/x-json], :charset => "utf-8")
Note that the addition of each MIME type takes an identifying symbol, a transformation method or nil, a list of Accept header values to associate with, and the response header to be sent out.
1.6.1.11 BootLoader::Cookies This boot loader mixes in cookie functionality in both controllers and requests: require 'merb-core/dispatch/cookies' Merb::Controller.send(:include, Merb::CookiesMixin) Merb::Request.send(:include, Merb::CookiesMixin::RequestMixin)
1.6.1.12 BootLoader::SetupSession This steps up the defined session containers and default values. Once again, here it’s best to take a look into the source code yourself if your interest is piqued. 1.6.1.13 BootLoader::AfterAppLoads Analogous to the before_app_loads boot loader, the after_app_loads boot loader calls all the callbacks collected from after_app_loads blocks: Merb::BootLoader.after_load_callbacks.each {|x| x.call }
From the Library of Shirong Chen
1.6
An overview of Merb internals
31
1.6.1.14 BootLoader::SetupStubClasses This boot loader creates stubbed classes for Merb::Application and Merb:: Exceptions. This is important in making sure exception controllers are available even in very flat applications: unless defined?(Exceptions) Object.class_eval []
There is another class that serves as a precursor to routes, called a behavior. Router behaviors make it possible for us to specify routes in a nested manner that grow toward completeness through the layering of both route conditions and parameters. As an application developer, you do not need to know of the existence of route behaviors even though you’ll be interacting with them unknowingly. On the other hand, for those of you destined to become plugin developers or possibly core contributors, it’ll serve you well to know that route behaviors are instances of Merb::Router::Behavior and that each block inside a router configuration is evaluated within the context of a parent behavior. Nested blocks imply nested behaviors, and these are compiled down to a flat ordered list at boot-up in the previously mentioned array Merb::Router.routes. From either perspective, what matters most when specifying routes is an understanding of the two elements that make up each complete route. These are the route conditions and the route parameters. Let’s take a look at each before we put them to use.
From the Library of Shirong Chen
2.1
How Merb routing works
39
2.1.1 Route conditions Route conditions are what the Merb router matches incoming requests against. In general, there are two minimal conditions a route must have: a path and a method. Paths are specified using strings, whereas method matching may use strings, symbols, or arrays. Getting the most out of route regular expressions You can also use regular expressions when setting route conditions. Doing so, however, may make the router incapable of creating an inverse mapping of the route, which prevents it from using the route statement for URL generation. The more modest alternative that also allows for route generation is to use regular expressions against the symbolic path segments that we’ll talk about soon. Merb also translates these regular expression matches into additional constraints for your routes, so overall, using them can be the best option.
2.1.1.1 Route method Sometimes requests going to the same path have different intents. For example, a request for /users/1 may sometimes be used to show the profile of a user, but at other times the request may include data used to update the user’s details. However, instead of guessing what the intent of the request is, based on what data is received, it is standard to rely on the request’s HTTP method as the definitive indicator of intent. A number of HTTP methods exist, but four in particular are used with Merb. These are GET, POST, PUT, and DELETE. In the context of REST, these methods are often called verbs. RESTfully speaking, each one plays a particular role well defined by both the W3C HT TP standards and RESTful intent. However, if you feel the need to set up routes that deviate from these standards, you have the freedom to do so. Just be warned: Using HTTP methods and their RESTful interpretations is more and more becoming a shared best practice, and going against it may only serve to complicate your application for both internal developers as well as web service consumers. Here’s a list of the four most important HTTP methods: • GET—the most common HTTP verb, used to request data without explicitly af-
fecting it • POST—the default verb for form submission, used to push arbitrary data onto the
server • PUT—used when data sent is intended to replace the existing data corresponding
with a particular URL • DELETE—used to request the deletion of data related to a particular URL
From the Library of Shirong Chen
40
Chapter 2: Routing
The case of the missing HTTP methods Note that some browsers do not produce all four of these HTTP verbs and are limited to only GET and POST. To make up for this and imitate full HTTP method compatibility, Merb uses both X-HTTP_METHOD_OVERRIDE and a hidden query parameter to enable the translation of particular POSTs into PUTs or DELETEs. In Chapter 1 we saw the boot loader that enabled these overrides. The point is, you won’t have to worry about the details of method overriding as an application developer. Merb will take care of it for you.
In the conditions hash of a route, a route method is typically indicated by use of one of the symbols :get, :post, :put, or :delete. You may instead use strings or regular expressions, but symbols are preferred. In the following code, we begin to define a route by specifying a method: Merb::Router.prepare do match(:method => :get) do # ... end end
2.1.1.2 Route path The more recognizable part of an HTTP request is its path. This includes everything after the domain of the request and before the query string. For example, the path of the request URL http://merbdeveloper.com/users/1 is /users/1. What about the query string? An HTTP path does not include the query string, which is everything that follows the first question mark in a URL. In general, query strings are used to pass in request parameters that may be used by the application code. For example, a request to /users?first_name=Foy may pull up a list of users with that first name only. Strictly speaking, though, the request parameters found in a query string are used only to drill down to the specifics of the response desired and do not play an intrinsic role in route definition. Nonetheless, Merb routes do allow you to translate parts of a route path into request parameters. We’ll see this often with resources routes. For example, /users/foysavas may, in the process of getting routed, translate the second segment into the value of params[:username].
Route paths are segmented by Merb with the standard slash separator or, as needed, by the end of symbolic segments. The two types of segments are literal and symbolic.
From the Library of Shirong Chen
2.1
How Merb routing works
41
Table 2.1 Route paths Route Path
Matching Path
Extracted Parameters
/users
/users
/users/:id
/users/1
:id => 1
/posts/:year_:month
/posts/08/08
:year => 08, :month => 08
Literal segments match literally against themselves. For instance, /users/new matches the first segment of the route path '/users/:action', which is '/users'. Symbolic segments, on the other hand, match any string that does not include a slash separator. For example, in the last example, the new at the end is matched against :action. As a side note, since :action has a special meaning among symbols, this route path automatically associates itself with the controller action named new in practice. To whet your appetite, Table 2.1 shows a few more examples of route paths and their interpretations by the router. When it comes time to write out our route path conditions, they will appear either first in match statements or as values to the key path: Merb::Router.prepare do match("/users") do # ... end match(:path => "/session") do # ... end end
2.1.1.3 Other conditions There are a few other conditions we can match against in route conditions. Two of these are protocol and domain, and they allow us, for instance, to match requests coming from secure HTTP or requested from a particular subdomain: Merb::Router.prepare do match(:protocol => 'https', :domain => 'secure.merbdeveloper.com') do # ... end end
From the Library of Shirong Chen
42
Chapter 2: Routing
Other conditions aside from path, protocol, domain, and method are assumed to be for symbolic segments and can be used to add constraints to segments through regular expressions. Here, the ID of a path matches only if it’s a string of digits: Merb::Router.prepare do match("/users/:id", :id => %r{ˆ[0-9]+}) do # ... end end
2.1.2 Route parameters Route parameters specify where the route should go. They are described as hashes where a few keys are interpreted separately, and all the rest are considered part of the parameters of the request itself. Here’s a list of the special keys: • controller—the controller to which the request will be routed in snake case • action—the action to which the request will be routed; defaults to “index” • format—the format in which the response should be given
All other route parameters do not have special meaning and are passed on as parameters, appearing with or, if necessary, overriding any parameters from elsewhere in the request. Here’s an example that uses both the special and nonspecial parameters: Merb::Router.prepare do match('/:controller/:action/:id'). to(:controller => controller, :action => :action, :id => id) end
One thing to note about this example is that all of the route parameters are redundant, as the Merb router translates symbolic segments into route parameters automatically.
What’s with this word parameter? You may be feeling the pain from this wildly overloaded word parameter. To clarify, there are two types of parameters within the context of a request plus the more general use of the word in Ruby:
• Route parameters---parameters that define where a request should be directed, or are inversely used to abstractly generate a URL. For example, a line such as to(:controller => 'users', action => 'new') inside router.rb uses router parameters within the to.
From the Library of Shirong Chen
2.2
Router configuration
43
• Request parameters---the collective data parameters of a request. These include the parameters found in a query string or as part of post data, as well as the nonspecial route parameters Merb merges in. These are all accessible from either the controller or the view as part of the mash params.
• (Method) parameters---our veritable friends that serve as data passed into a method.
2.2 Router configuration Though we’ve seen some examples, up until now we’ve spoken only conceptually of routes, so it’s time to create our own configurations and learn about all the available methods. Routes are specified within a configuration block passed to the router for interpretation. Depending on the layout of your application, this configuration block may be in different locations, but it’s important that it gets loaded at boot-up so the routes can be compiled.
2.2.1 The router file In a standard Merb layout, routes are specified in the file config/router.rb. Single-file Merb applications include a router configuration near the top of the file. Flat layouts put it in application.rb. With either of these, router configuration code looks just the same and works through the passing of a block to the class method Merb::Router.prepare. Here’s the content from a standardly generated config/router.rb file less comments: Merb.logger.info("Compiling routes...") Merb::Router.prepare do default_routes end
The first line, of course, is just a log message and is not critical to our routes. The next class method is the one we previously referenced, and in the default case of a generated standard application it has only default_routes. We’ll get into what this does soon.
2.2.2 The prepare block The prepare block works by evaluating each line in the context of a route behavior. Remember, route behaviors are basically nestable and are not necessarily complete
From the Library of Shirong Chen
44
Chapter 2: Routing
precursors to routes. For instance, the root behavior for the prepare block is internally defined as Behavior.new.defaults(:action => "index")
All deeper route behaviors are based on the behavior created by the method before the nesting of a block. This is demonstrated by the following prepare block: Merb::Router.prepare do to(:controller => 'home') do match('/') match('/preview').to(:action => :preview) end end
The two match lines are evaluted within the context of a new child behavior where the following route parameters have been set: :controller => 'home', :action => 'index'
Note that the second match statement overrides the action of the parent behavior, whereas the first relies on its parent entirely to specify its route parameters.
2.2.3 Route order All behaviors are compiled down into a flat list on boot-up, and when the router is used to recognize or generate a URL, it’ll work with the first possible route it finds. This often trips up first-timers, especially via the line default_routes: Merb::Router.prepare do default_routes match('/users/:id').action(:action => 'show') end
In this example, the default_routes will always take precedence, and therefore the match on the second line will never work. Be sure to pay attention to the order of your routes to avoid this problem.
2.2.4 Adding routes later on Typically, as an application developer you won’t need to add routes outside the prepare block, but it is still possible. To do this, you can use the two class methods
From the Library of Shirong Chen
2.3
Checking routes
45
Merb::Router.append and Merb::Router.prepend. Both of these work exactly like
the prepare block but insert their routes before or after all the others: Merb::Router.append do match('/info').to(:controller => 'info') end
2.3 Checking routes Sometimes you’ll want to check the routes of your application to make sure requests are routed properly. There are a number of ways to do this. So that you can use these methods to test the various router configurations we describe later and the router configurations of your own applications, we’ll describe them here. For all of this section’s examples we’ll use a standard Merb application with the following routes defined in config/router.rb: Merb::Router.prepare do # RESTful routes resources :posts # This is the default route for /:controller/:action/:id # This is fine for most cases. If you're heavily using resource-based # routes, you may want to comment/remove this line to prevent # clients from calling your create or destroy actions with a GET default_routes # Change this for your home page to be available at / match('/').to(:controller => 'whatever', :action =>'index') end
2.3.1 Listing routes Getting a simple list of the compiled routes can do wonders in determining why a request has gone astray. We’ll do this first using interactive Merb and then using a rake task.
2.3.1.1 Using interactive Merb To list routes in interactive Merb, simply start up the application and issue the command Merb::Router.route. Here we pull up a list of the routes in interactive Merb using the previously defined routes: > pp Merb::Router.routes [/posts(/index)(.:format), /posts(.:format), /posts/new(.:format),
From the Library of Shirong Chen
46
Chapter 2: Routing
/posts/:id(.:format), /posts/:id/edit(.:format), /posts/:id/delete(.:format), /posts/:id(.:format), /posts/:id(.:format), /:controller(/:action(/:id))(.:format), /] => nil
The first eight come from the resources route and are perfect examples of how effective resources routing can be in producing the routes you need. These routes come first because resources :posts does, too. Following those eight routes is the default_routes route. Notice that it translates to only one route, but given its complexity and versatility it has kept its name. Finally, the last route, the single /, matches a request for the root or empty path. Each of these items in the routes array appears as the path string above. However, don’t let that deceive you, since they’re all full routes that you can play around with. Below, we interact with the conditions and parameters for the default_routes route. > Merb::Router.routes[8].conditions => {:path=>/ˆ\/([ˆ\/.,;?]+)(?:\/([ˆ\/.,;?]+) (?:\/([ˆ\/.,;?]+))?)?(?:\.([ˆ\/.,;?]+))?$/} > Merb::Router.routes[8].params => {:controller=>"(path1)", :format=>"(path4)", :action=> "(path2 || \"index\")", :id=>"(path3)"}
As we’ll learn shortly, routes also come with names. These are either selected by you or they default to the name “default.” Below we use interactive Merb to list the named routes for us. > pp Merb::Router.named_routes {:new_post=>/posts/new(.:format), :posts=>/posts(/index)(.:format), :default=>/:controller(/:action(/:id))(.:format), :edit_post=>/posts/:id/edit(.:format), :delete_post=>/posts/:id/delete(.:format), :post=>/posts/:id(.:format)} => nil
Nearly all of these are for the resources route that was specified. Note that the inflector has been used to create these names, thus allowing us to naturally generate URLs for the various RESTful interactions. Also of importance is the precedence of the default_routes route over the specified root route. The former has become the route attached to the default name.
From the Library of Shirong Chen
2.3
Checking routes
47
Alternatively we can use the convenience method merb.show_routes to display all the routes in a significantly more verbose yet less interactive way. Here we pull up the same routes using this method: > merb.show_routes ==== Named routes Helper : new_post HTTP method: GET Route : /posts/new(.:format) Params : {:controller=>"\"posts\"", :format=>"(path1)", :action=>"\"new\""} Helper : posts HTTP method: GET Route : /posts(/index)(.:format) Params : {:controller=>"\"posts\"", :format=>"(path1)", :action=>"\"index\""} Helper : default HTTP method: GET Route : /:controller(/:action(/:id))(.:format) Params : {:controller=>"(path1)", :format=>"(path4)", :action=>"(path2 || \"index\")", :id=>"(path3)"} Helper : edit_post HTTP method: GET Route : /posts/:id/edit(.:format) Params : {:controller=>"\"posts\"", :format=>"(path2)", :action=>"\"edit\"", :id=>"(path1)"} Helper : delete_post HTTP method: GET Route : /posts/:id/delete(.:format) Params : {:controller=>"\"posts\"", :format=>"(path2)", :action=>"\"delete\"", :id=>"(path1)"} Helper : post HTTP method: GET Route : /posts/:id(.:format) Params : {:controller=>"\"posts\"", :format=>"(path2)", :action=>"\"show\"", :id=>"(path1)"} ==== Anonymous routes HTTP method: POST Route : /posts(.:format) Params : {:controller=>"\"posts\"", :format=>"(path1)", :action=>"\"create\""}
From the Library of Shirong Chen
48
Chapter 2: Routing
HTTP method: PUT Route : /posts/:id(.:format) Params : {:controller=>"\"posts\"", :format=>"(path2)", :action=>"\"update\"", :id=>"(path1)"} HTTP method: DELETE Route : /posts/:id(.:format) Params : {:controller=>"\"posts\"", :format=>"(path2)", :action=>"\"destroy\"", :id=>"(path1)"} HTTP method: GET Route : / Params : {:controller=>"\"whatever\"", :action=>"\"index\""} => nil
In this output, Helper signifies the name that should be used when we look to generate the URL. If you take a look at the penultimate and antepenultimate routes, it’s clear why two routes have appeared previously with the exact same path: One route is for PUT requests, which go to the update action, and the other is for DELETE requests, which go to the destroy action.
2.3.1.2 Using the audit routes rake task Standard applications are generated with a rake task that lists all routes for us without our having to use interactive Merb. To do this we can step into the Merb application’s root directory and issue the following command: $ rake audit:routes
Given the routes that were previously defined, the following routes are displayed: Named Routes delete_post: /posts/:id/delete(.:format) posts: /posts(/index)(.:format) edit_post: /posts/:id/edit(.:format) default: /:controller(/:action(/:id))(.:format) post: /posts/:id(.:format) new_post: /posts/new(.:format) Anonymous Routes /posts(.:format) /posts/:id(.:format) /posts/:id(.:format) /
From the Library of Shirong Chen
2.3
Checking routes
49
2.3.2 Trying the router Sometimes the only way to figure out what’s going on is to ask the router directly. Here we use interactive Merb to check both URL recognition and generation.
2.3.2.1 For URL recognition Using the Merb convenience methods, we can first put together a fake request and then see where the route will take it. The method merb.fake_request makes it easy for us to put together a request by just overriding default values. Here are the default settings that make up a merb.fake_request: > pp Merb::Test::RequestHelper::FakeRequest::DEFAULT_ENV {"SERVER_NAME"=>"localhost", "PATH_INFO"=>"/", "HTTP_ACCEPT_ENCODING"=>"gzip,deflate", "HTTP_USER_AGENT"=> "Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.1) Gecko/20060214 Camino/1.0", "SCRIPT_NAME"=>"/", "SERVER_PROTOCOL"=>"HTTP/1.1", "HTTP_CACHE_CONTROL"=>"max-age=0", "HTTP_ACCEPT_LANGUAGE"=> "en,ja;q=0.9,fr;q=0.9,de;q=0.8,es;q=0.7,it;q=0.7,nl; q=0.6,sv;q=0.5,nb;q=0.5,da;q=0.4,fi;q=0.3,pt;q=0.3,zh -Hans;q=0.2,zh-Hant;q=0.1,ko;q=0.1", "HTTP_HOST"=>"localhost", "REMOTE_ADDR"=>"127.0.0.1", "SERVER_SOFTWARE"=>"Mongrel 1.1", "HTTP_KEEP_ALIVE"=>"300", "HTTP_REFERER"=>"http://localhost/", "HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.7", "HTTP_VERSION"=>"HTTP/1.1", "REQUEST_URI"=>"/", "SERVER_PORT"=>"80", "GATEWAY_INTERFACE"=>"CGI/1.2", "HTTP_ACCEPT"=> "text/xml,application/xml,application/xhtml+xml,text/ html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", "HTTP_CONNECTION"=>"keep-alive", "REQUEST_METHOD"=>"GET"} => nil
We can override the settings by passing in parameters. With the following, we modify the default fake request so as to imitate the deletion of a post: > fr = merb.fake_request('REQUEST_PATH'=>'/posts/1',' REQUEST_METHOD'=>'DELETE')
From the Library of Shirong Chen
50
Chapter 2: Routing
With our fake request ready, we can now use another convenience method to check if it routes to where it should. This method is merb.check_request_for_route and takes a request (fake or not) as its sole parameter: > merb.check_request_for_route(fr) => {:controller=>"posts", :format=>nil, :action=>"destroy", :id=>"1"}
The hash returned says that the request does in fact route to the destroy action of the posts controllers, so everything is in order.
2.3.2.2 For URL generation We can also test the generation of routes using another convenience method, merb.url. It takes a route name plus route parameters. Leaving out the route name sets it to default. Below we test the generation of the URL for editing the first post and then for an anonymous default route. > merb.url(:edit_post,:id => 1) => "/posts/1/edit" > merb.url(:controller => 'home', :action => 'login') => "/home/login"
2.4 Match rules Match rules are the basis for defining all route definitions. Let’s explore the various possibilities.
2.4.1 Literal matching Literal matching is quite simple. Using the route behavior method match within the block given to Merb::Router.prepare, we can match against literal strings: Merb::Router.prepare do match('/examples/routes') end
You can also nest literal match statements by passing in blocks. The following example demonstrates the same match as before: Merb::Router.prepare do match('/examples') do match('/routes') end end
From the Library of Shirong Chen
2.4
Match rules
51
Literal matches occur only if the string matches the entirety of the request path. For example, if a request comes in with the path /examples/controllers', it will not match against '/examples'. The exception, of course, is if a nested match completes the literal path. Remember that the route behaviors listed in the route are compiled before use, so nested literal matches are concatenated in their final form. Finally, an uncommon alternative to simply passing in the literal string is to instead pass in a hash with a value on the key :path. We provide this example so that you may recognize such usage if you spot it: Merb::Router.prepare do match(:path => '/examples') end
2.4.2 Symbolic matches Symbolic matches use symbols to more abstractly match request paths and associate these matches with parameters. These symbols appear as substrings within match path strings and can have additional rules set upon them using regular expressions. By default, a symbol matches all alphanumeric characters, the dash, and the underscore in a nongreedy fashion. Below is a simple example of a match rule that automatically sets the controller and action. Note that the colon is not allowed in URL paths, so there’s no need to worry about ambiguity. Merb::Router.prepare do match('/:controller/:action') end
Overall, the use of symbolic segments effectively extends the matches in three ways. Let’s go over these individually.
2.4.2.1 Automatic parameters Using symbolic match, we can tersely specify both route conditions and parameters. This works by mapping each symbolic match to an equivalent route parameter. Below, we redundantly route a symbolic match to where it was going even without the to statement. Merb::Router.prepare do match('/:controller').to(:controller => controller) end
From the Library of Shirong Chen
52
Chapter 2: Routing
2.4.2.2 Flexible segmentation Symbolic matches allow us to segment routes without relying on slash separators. Below we pull out a year, month, and day separated by dashes. Merb::Router.prepare do match('/dates/:year-:month-:day').to(:controller => 'dates') end
This match rule matches and segments request paths that look like this: /dates/2008-10-11 /dates/sorry-try-harder
In both of these examples, the three symbolic segments are translated into route parameters under the names :year, :month, and :day.
2.4.2.3 Segment-specific regular expressions We just saw an example where the match rule ended up matching possibly unwanted segments and translated them into route parameters. We can set further limits on specific segments by using regular expressions, which are compatible with both route recognition and generation and should be used when possible. Here we limit the previous example so that only strings of digits are allowed: Merb::Router.prepare do match('/dates/:year-:month-:day').to( :controller => 'dates', :year => /\d+/, :month => /\d{1,2}/, :day => /\d{1,2}/ ) end
2.4.3 Optional matches Using parentheses, we can tell the router that certain parts of a match are optional. This can significantly lessen the number of rules we need to write. We’ve seen this already with the case of the so-called default_routes, which are actually defined with just one match but using a number of parentheses: match("/:controller(/:action(/:id))(.:format)")
Note that some of the parentheticals are embedded within other parentheticals. The following routes match the preceding match rule: /users /posts/delete
From the Library of Shirong Chen
2.4
Match rules
53
/projects/edit/3 /groups.xml /users/new.txt /posts/update/6
2.4.4 Full regular expressions Though it’s not advisable because it may leave you unable to generate URLs from route parameters, you can directly use a regular expression to match the full path of a request: Merb::Router.prepare do match(%r[/account/([a-z]{4,6})]).to(:controller => "account", :action => "show", :id => "[1]") end
Using interactive Merb on an application where this is the only route, we can confirm that though the route is recognized and mapped to parameters, it cannot inversely be generated by those parameters: > merb.check_request_for_route(merb.fake_request( 'REQUEST_PATH'=>'/account/abcd')) => {:controller=>"account", :action=>"show", :id=>"abcd"} > merb.url(:controller => 'account', :action => 'show', : id => 'abcd') Merb::Router::GenerationError: Named route not found: default
2.4.5 Deferred routes Deferred routes allow you to incorporate more complex logic into your routes. Again, though, this comes at the cost of being unable to generate those routes via route parameters. Below we route requests to an Ajax controller if they are XML HTTP requests (aka Ajax requests). defer_to do |request, params| params.merge! :controller => 'ajax' if request.xhr? end
Note the block parameters that defer_to receives. These are the requests from the client and the parameters from the behavior context in which they are being evaluated. This may suggest that deferred routing may also appear nested within other route statements: Merb::Router.prepare do match('/').to(:controller => 'home') do defer_to do |request, params| if Time.now.hour < 6
From the Library of Shirong Chen
54
Chapter 2: Routing
params[:action] = 'night' else params[:action] = 'day' end end end end
Although this example demonstrates the nesting of defer_to, we definitely don’t recommend using it for such a purpose. In fact, we recommend that you stay away from using deferred routing as much as possible, leaving such logic for inside your controllers.
2.5 Registering routes In Merb, the process of assigning route parameters to matches is known as the route registration. This is a necessary step in assuring that behaviors are compiled down and put into the routes array. Merb uses three equivalent methods to do this. Each varies only in name but thus provides a better semantic feel in different contexts.
2.5.1 Using to The to method is the standard way of both setting route parameters and registering them. We’ve seen the to method in past examples, and though its usage may seem obvious enough, we’ll spend some time on the details. The to method takes one optional parameter, a hash representing route parameters that are merged into existing contextual route parameters. For instance, here’s the second to method that adds in an additional route parameter to what was established by the first: Merb::Router.prepare do match('/users').to(:controller => 'users') do match('/:id').to(:action => 'show') end end
This example also displays that the to method accepts a block. This block works similarly to a block passed to the match method. More specifically, the block is evaluated in a new context where the parent behavior has been extended through the parameters that were given. Formalities aside, you can also rely on the descriptive nature of methods themselves to understand that the previous example compiles down to the following route: ==== Anonymous routes HTTP method: GET Route : /users/:id
From the Library of Shirong Chen
2.5
Registering routes
55
Params : {:controller=>"\"users\"", :action=>"\"show \"", :id=>"(path1)"}
2.5.2 Using with The with method, which is just an alias of to, is semantically used in forming blocks around routes without being chained to a specific behavior. Here we wrap with around a set of matches that are passed in a block: Merb::Router.prepare do with(:controller => "users") do match("/signup").to(:action => "signup") match("/login").to(:action => "login") match("/logout").to(:action => "logout") end end
These, of course, all compile down to routes set to be handled by the Users controller but by different actions: ==== Anonymous routes HTTP method: GET Route : /signup Params : {:controller=>"\"users\"", :action=>"\"signup\""} HTTP method: GET Route : /login Params : {:controller=>"\"users\"", :action=>"\"login\""} HTTP method: GET Route : /logout Params : {:controller=>"\"users\"", :action=>"\"logout\""}
Note that either to or register, which we’re about to discuss, could have been used in place of with, but we use with because it reads well aloud.
2.5.3 Using register The most common use of register, which is once again an alias of to, is to assure that a behavior is registered into a route even when no other route parameters need to be set. The reason this exists is because often the match method will automatically have set all your parameters for you and an empty .to would look awkward. Below we register such a behavior into a route.
From the Library of Shirong Chen
56
Chapter 2: Routing
Merb::Router.prepare do match("/:controller(/:action(/:id))(.:format)").register end
Note again, however, that register could have been to or even with and have had the same effect.
2.5.4 Redirects You can redirect routes from inside the Merb router. This is useful in maintaining compatibility after you’ve changed routes on a production application and want to assure that the bookmarks of users still work. To register a redirect, simply use the redirect method on a behavior. Note that you may also specify whether the redirect is permanent or not. If it is, the redirect will be sent with a status code of 301 instead of 302. Merb::Router.prepare do match('/stories').redirect('/articles', :permanent => true) end
2.5.5 Using symbols As we’ve seen previously, symbolic matching implicitly sets route parameters for us. However, we can also manually do so ourselves. Symbolic segment values are available as symbols within the strings used on values of the hash given to a to or any of its aliases. The following example should make this clearer: Merb::Router.prepare do match("/users/:last_name/:first_name").to( :controller => 'users', :full_name => "#{first_name} #{last_name}") end
To check that it works, we can fire up interactive Merb and send a fake request to the path /users/savas/foy: > merb.check_request_for_route(merb.fake_request( 'REQUEST_PATH'=>'/users/savas/foy')) => {:controller=>"users", :full_name=>"foy savas", :last_name=>"savas", :action=>"index", :first_name=>"foy"}
2.5.6 Using other captures We can also use other match captures in the value strings of the to hash. Bracketed numbers allow us to pull up either regex captures or symbolic segment values in the
From the Library of Shirong Chen
2.6
Other route settings
57
order in which they were matched, starting with 1. In the following example, we match the first segment as the language, then use default routes nested within: Merb::Router.prepare do match(%r{/?(en|es|fr|be|nl)?}).to(:language => "[1]") do default_routes end end
2.6 Other route settings You can do a few more standard things with routes, including setting defaults for route parameters and naming routes for references during generation. Let’s take a look at both of these.
2.6.1 Setting defaults Default values allow you to set defaults for route parameters that have not been set. To do this, we apply the default method on a behavior. For instance, below we set the controller match segment to optional but default it for Users. Note that we have to end the line with register, because default does not register the route itself. Merb::Router.prepare do match("(/:controller)/:id", :controller => /users|groups/). default(:controller => 'users').register end
We can test the results of this route registration using interactive Merb. Here it behaves as intended: > merb.check_request_for_route(merb.fake_request(' REQUEST_PATH'=>'/groups/robots')) => {:controller=>"groups", :action=>"index", :id=>"robots"} > merb.check_request_for_route(merb.fake_request(' REQUEST_PATH'=>'/robot')) => {:controller=>"users", :action=>"index", :id=>"robot"} > merb.check_request_for_route(merb.fake_request(' REQUEST_PATH'=>'/machines/robots')) Merb::ControllerExceptions::BadRequest: No routes match the request. Request uri: /machines/robots
2.6.2 Named routes Merb can identify routes by “name.” These names are convenient identifiers often used in the generation of URLs. Most of the routes we’ve used so far have gone unnamed.
From the Library of Shirong Chen
58
Chapter 2: Routing
However, we can easily add names by passing in a symbol, the name method: Merb::Router.prepare do match('/').to(:controller => 'home').name(:home) end
In interactive Merb, we can generate the root URL by passing the symbol :name to merb.url: > merb.url(:home) => "/"
Wasted typing? Of course, using a name to generate the root path may seem unnecessarily lengthy, but in practice this is not true. Named routes allow us to abstractly identify routes, whether short or long, and thus separate our need to determine what a route should look like from our need to use it in links, redirects, and elsewhere. Consequently, if at one point we decide to redefine the paths associated with particular routes, we will need to make changes only in the router. Now that’s time saved.
Remember that the url method is also mixed into Merb::Controller, so we’ll be using it heavily within both controllers and views to generate URLs for links and set the paths of redirects.
2.6.3 Setting prefixes We can prefix both names and controllers using routes. In the case of names, this means prepending a string plus an underscore to the name; this is most commonly used to semantically group associated routes. With controllers, this entails that the controller is found within a module. Let’s go through how to accomplish both of these types of prefixes.
2.6.3.1 Name prefix There are two ways to add a name prefix. The first is by passing in an extra symbol parameter to the name method: Merb::Router.prepare do match('/admin').to(:controller => 'admin').name(:admin, :home) end
From the Library of Shirong Chen
2.6
Other route settings
59
Alternatively we can use the options method: Merb::Router.prepare do match('/admin').to(:controller => 'admin').name(:home). options(:name_prefix => :admin) end
2.6.3.2 Controller prefix Merb allows you to nest controllers within modules and thus bundle sets of controllers with a similar purpose. For instance, we may nest all admin functionality within a module named Admin. In the following example, we make the router aware of such a setup: Merb::Router.prepare do match('/admin').option(:controller_prefix => :admin) do match('/:controller(/:action)').register end end
Here, paths like /admin/users route to the controller Admin::Users.
2.6.3.3 Namespaces You can achieve the effect of both name and controller prefixes more easily by using the method namespace. This is perfect for an admin module where you also want to be able to use named routes like admin_users to point to /admin/users. Below we use namespace to set both the name and the controller prefix. Merb::Router.prepare do namespace(:admin) do match('/:controller(/:action)').register end end
2.6.4 Fixatable routes Sometimes you need a route to be able to include a session ID. This is known as session fixation and is considered insecure for most purposes. The reason is that when you allow a session ID to be included in a URL, one user could trick another user into following a link with a fixated session. Once this second user logs in, the first user will be able to hijack the second user’s account. Merb therefore does not allow for fixated sessions by default, but does allow for them in certain cases so that they can pass on session-related responses to clients (like Adobe
From the Library of Shirong Chen
60
Chapter 2: Routing
Flash Player when it’s used to upload files) that are unable to handle cookies. Here’s an example of a fixatable route: Merb::Router.prepare do match('/file_uploads').to(:controller => 'files', : action => 'upload').fixatable end
Please remember to use fixatable routes only when needed, and to assure session integrity, regenerate the session ID after use.
2.7 Resource routes Resource routes are RESTful routes that provide a standard way of interacting with resources. Let’s explain how resources work in terms of the router.
2.7.1 Standard resources routing The method resources sets up various routes that allow you to access a particular resource. These routes are similar to the default routes but do not match exactly. For instance, with a default route you might use /users/show/1 to see the profile of a user. With a resources route, however, you would use /users/1. Resources routes also set up various defaults for the creation, modification, and deletion of resources. They also name each of your routes, making it easy to generate URLs. Let’s take a look at the routes generated by the following prepare block: Merb::Router.prepare do resources :turtles end
In interactive Merb we can pull up the following list: > merb.show_routes ==== Named routes Helper : delete_turtle HTTP method: GET Route : /turtles/:id/delete(.:format) Params : {:format=>"(path2)", :controller=>"\"turtles\"", :action=>"\"delete\"", :id=>"(path1)"} Helper : new_turtle HTTP method: GET Route : /turtles/new(.:format) Params : {:format=>"(path1)", :controller=>"\"turtles\"", :action=>"\"new\""}
From the Library of Shirong Chen
2.7
Resource routes
61
Helper : turtles HTTP method: GET Route : /turtles(/index)(.:format) Params : {:format=>"(path1)", :controller=>"\"turtles\"", :action=>"\"index\""} Helper : turtle HTTP method: GET Route : /turtles/:id(.:format) Params : {:format=>"(path2)", :controller=>"\"turtles\"", :action=>"\"show\"", :id=>"(path1)"} Helper : edit_turtle HTTP method: GET Route : /turtles/:id/edit(.:format) Params : {:format=>"(path2)", :controller=>"\"turtles\"", :action=>"\"edit\"", :id=>"(path1)"} ==== Anonymous routes HTTP method: POST Route : /turtles(.:format) Params : {:format=>"(path1)", :controller=>"\"turtles\"", :action=>"\"create\""} HTTP method: PUT Route : /turtles/:id(.:format) Params : {:format=>"(path2)", :controller=>"\"turtles\"", :action=>"\"update\"", :id=>"(path1)"} HTTP method: DELETE Route : /turtles/:id(.:format) Params : {:format=>"(path2)", :controller=>"\"turtles\"", :action=>"\"destroy\"", :id=>"(path1)"}
Note that all resources go to the same controller, but that there are seven standard actions: index, new, create, edit, update, destroy, and show. We’ll learn about these seven actions and their purpose in depth when we cover REST, but for now, it should suffice to know which routes go to which action. Here’s a list of examples with a brief explanation of intent:
2.7.1.1 Index • /users—retrieves a list of users • /turtles.xml—retrieves a list of turtles as XML
From the Library of Shirong Chen
62
Chapter 2: Routing
2.7.1.2 New • /users/new—requests the form needed to create a new user account
2.7.1.3 Create • /users with POST—creates a new user from POST data • /turtles.txt—creates a new turtle with POST data with confirmation in a text
format
2.7.1.4 Edit • /users/1/edit—requests the form needed to create a new user account
2.7.1.5 Update • /users/1 with PUT—updates the user with an ID equal to 1 with the POST data • /turtles/1.json with PUT—updates the turtle with an ID equal to 1 with the
POST data with confirmation in JSON
2.7.1.6 Destroy • /users/1 with DELETE—deletes the user with an ID of 1 • /turtle/1/delete with GET—deletes the turtle with an ID of 1
2.7.1.7 Show • /users/1—shows the profile of the user with an ID of 1 • /turtle/1.xml with GET—shows the details of the turtle with an ID of 1 in XML
2.7.2 Singular resource routing Sometimes a resource is all alone. In other cases the user’s interaction with that class of resources should be limited to only the one that belongs to that user. In both cases, a singular resource route is best. Here we use a singular resource route for our application’s overall admin settings (stored as one row in a table) and to set up routes for our user’s account, both times using the resource method: Merb::Router.prepare do namespace :admin do resource :settings end
From the Library of Shirong Chen
2.7
Resource routes
63
resource :account end
This creates routes similar to those created by resources but without IDs. The singularity of the resource also rules out the need for an index action. Thus the only actions routed to are show, new, create, edit, update, and destroy.
2.7.3 Using identify Sometimes a route like /users/1 is unpleasant. In fact, there are at least four reasons why such a URL may be unwanted: • You don’t want to have to remember ID numbers to see a resource’s page. • Search engines often optimize for keywords in URLs, so ID numbers are bad search
engine optimization. • ID numbers can give away how many of a resource you have. • ID numbers make scraping your data from the web easy to do.
To get around these issues, the Merb router has a method identify. By using it you can change how a resource is turned into a string in routes. Here we use identify to opt for the use of username instead of id: Merb::Router.prepare do identify User => :username do resources :users end end
Thus, routes generated with a user object use the username method for identification. url(:users, user) # => ex. '/users/foysavas'
We can also provide custom methods within our models to stringify our identifier. This method has to be defined in your model class. For those using the ActiveRecord ORM, the method to_params is supposed to serve this purpose. Consequently, the ActiveRecord plugin saves you from even having to include identify blocks in your router by automatically wrapping the router in an identify ActiveRecord::Base => :to_params.
From the Library of Shirong Chen
64
Chapter 2: Routing
2.8 Conclusion Routing is critical to an MVC web framework. Merb applications enjoy simple-to-use, semantically well-defined route definition that boosts comprehensive bidirectional route recognition and generation. We’ve also gotten a taste of what it means to be RESTful and why adherence to such standards can simplify both an application’s internals and its API. Finally, we even peered into the Merb routing internals to understand how the behavior class is used as an intermediate step toward route definition.
From the Library of Shirong Chen
C HAPTER 3
Controllers
In Merb, controllers are what tie all of your application code together. They are the C in MVC and have been built to assemble responses to the requests received from users. On occasion, a controller’s response is entirely produced by code in the controller itself. More often, though, responses are complex composites that incorporate the pulling of data through models and the rendering of views through templates. Controllers also handle the more subtle aspects of responses, including response format, MIME type, and status code. We’ll also craft within them behind-the-scenes logic that uses sessions, filters, and exceptions. When users request unavailable or impermissible data, controllers can redirect those users to more appropriate locations. Finally, Merb controllers come with the built-in ability to queue up background processes and run them outside the standard request-to-response cycle. In this chapter we will go over each of these topics as well as controller internals. While doing this, we’ll emphasize controllers themselves and defer examples involving models and views to later chapters. This may seem unconventional, but separating these concerns will, if nothing else, make this chapter an invaluable reference when all you need to do is cut to the chase. That said, there will be some blurring of focus near the end of this chapter as it weaves into the next and we discuss the rendering of views through controllers. If you’re completely new to MVC, controllers are definitely the most natural and best place to start, because they let you dig right in and produce responses from the get-go. To keep things simple, nearly all the examples in this chapter are going to be single-file Merb applications that can be run easily alongside a default set of routes.
3.1 From request to controller Since we previously went over the Merb router, it’s a good time to walk through how requests get to controllers and how controllers set up actions to handle those requests. 65 From the Library of Shirong Chen
66
Chapter 3: Controllers
3.1.1 A simple application While Merb controller actions are just standard methods of classes you can initialize and use, a Merb application itself does not call these actions. Instead, Merb initializes controllers based upon requests and then uses the controller method _dispatch to call the appropriate action. Notice that _dispatch starts with an underscore. As an application developer you don’t really need to know about it, but by using it you will avoid conflicts in the namespace of application-specific controller methods. However, we mention it here because of its central role in how controller instances deal with requests. That said, a basic understanding of both Merb::Request and _dispatch is desirable. So let’s fire up interactive Merb with the following application and imitate a request from the browser: class HelloFriend < Merb::Controller def index if params[:name] "hi #{params[:name]}" else "hi" end end end
3.1.2 How requests get dispatched When a Merb::Request is initialized, it requires at least two options to make sure it gets routed correctly: REQUEST_PATH and REQUEST_METHOD. True to their names, these two options represent the path and the method of the request. Let’s make a series of requests and observe their responses. To do this, we’ll use the Merb::Request#handle, which uses the router to first determine the appropriate controller and its action and then dispatch the request. Because we’re interested only in the body of the response, we’ll tack that onto our statement. $ merb -i -I home.rb > request = Merb::Request.new({'REQUEST_PATH'=>'/ hello_friend', 'REQUEST_METHOD'=>'GET'}) > request.handle.body => 'hi' > request = Merb::Request.new({'REQUEST_PATH'=>'/ hello_friend', 'REQUEST_METHOD'=>'GET', QUERY_STRING=> 'name=you'}) > request.handle.body => 'hi you'
From the Library of Shirong Chen
3.1
From request to controller
67
The second request we created used QUERY_STRING. Again, as an application developer, it’s not essential for you to know about the options that define a request, but QUERY_STRING, had it come from a browser, would be the text that follows the question mark in a URL such as http://localhost:4000/hello_friend?name=you. The end effect, with respect to the controller, is the setting of params[:name] equal to 'you'.
3.1.3 The controller’s perspective Let’s imitate another aspect of request handling by creating the controller ourselves instead of using Merb::Request#handle. We won’t need REQUEST_PATH and REQUEST_METHOD, since in this example we’ll be doing the job of the dispatcher and router ourselves when we choose and initialize the appropriate controller and then invoke the right action: > request = Merb::Request.new({'QUERY_STRING'=>'name=friend'}) > controller = HelloFriend.new(request) > controller.index => "hi friend"
Notice that this does not return a full response as in the previous examples but instead simply the body. This is because the job of each controller action is simply to return the body for the response. As we’ll discuss later on, the body can be more complex than a string and may use view template rendering through the method Merb::Controller#render. Aside from the body, the remainder of the response is typically set through various side effects of the controller code. To replicate the full response in interactive mode, we can more closely follow the path that the dispatcher takes and invoke the handling of a request with the Merb::AbstractController#_dispatch method we spoke of earlier: > request = Merb::Request.new({'QUERY_STRING'=>'name=there'}) > controller = HelloFriend.new(request) > controller._dispatch(:index).body => "hi there"
The second line initializes the HelloFriend controller. Notice how it requires a request as part of initialization. This is essential, as it allows the controller object to be aware of the request it is handling and interpret its parameters. Finally, we use _dispatch(:index) to indirectly perform the index action, and body to give us only the body of the return. There are two other major concerns that _dispatch takes care of, sessions and filters, both of which we will discuss later on.
From the Library of Shirong Chen
68
Chapter 3: Controllers
3.2 The controller classes All controllers in Merb inherit from the class Merb::AbstractController. This class was designed to allow the Merb core to be as extensible as possible. This is because, despite the fact that MVC and web frameworks now seemingly go hand in hand, the concept of the controller can apply well outside the web request controller. For example, sometimes you’ll want a controller class to manage the sending of emails. Other times, reminiscent of the original inspiration for the MVC design pattern, you’ll want a controller to determine the behavior of a particular widget embedded in the context of a response’s larger view template. Both of these are possible thanks to Merb’s abstract controller. At this point, if you feel a bit lost with regard to abstract controllers, don’t worry. What’s important to know right now is that all Merb controllers are actually subclasses of either Merb::AbstractController or one of its descendants. Within the Merb core, these descendants are Merb::Controller, Application, and Exception. Interestingly enough, these three classes, in the order in which they were just listed, form a descending chain of subclasses. In other words, Exception is a subclass of Application, which is a subclass of Merb::Controller, which itself is a subclass of Merb::AbstractController. We’ll go over each of these in this section and then describe how to use them in the rest of the chapter. In later chapters we’ll also see two more subclasses of Merb::AbstractController—MailController and PartController—both of which exemplify the ability of Merb::Abstract Controller to be subclassed in different ways for extremely practical purposes. However, before we get into these specific controller classes and their methods, it’s important to know that many of the methods you’ll find in the source of each of these classes begin with an underscore. This is because Merb controller classes seek to avoid populating themselves with method names you may otherwise want to use. Additionally, methods that start with underscores are understood to be outside the scope of the Merb public API and therefore don’t really need the attention of the application developer. With all this in mind, please note that we are simply enriching your understanding of the framework internals. Finally, you should know that some controller instance methods are called actions. This is because they are responsible for the act of responding to requests. In general, if you’re wondering whether a method is available as an action or not, you should first check if it is a public instance method. If it is, then it’s probably an action. There is, however, one exception, and that’s if the method has been added to the list of nonaction methods. This list is maintained so that you can prevent users of your application from sending requests directly to these methods. Not so surprisingly, most public instance methods in the core controller class appear in this list.
From the Library of Shirong Chen
3.2
The controller classes
69
Table 3.1 General methods Method
Use
controller_name
Returns the controller’s name in snake case with a / separating module namespaces
inherited
Adds a subclasses controller name to subclasses_list
subclasses_list
Holds a list of subclasses
3.2.1 The abstract controller Despite being the eldest parent class of all controllers, Merb::AbstractController makes very few specific demands on its descendants, leaving most of its methods as nothing more than stubs. Ultimately this means that Merb::AbstractController, true to its name, acts more as an ideal form of what a controller should look like than anything else. Let’s go over the prototypical methods that have been included in Merb:: AbstractController. First we’ll look at class methods and then instance methods.
3.2.1.1 Class methods There aren’t many API class methods in Merb::AbstractController, but those that do exist well characterize what a controller is all about. They also give you an idea of what sort of attributes and logic can be applied throughout a controller class and across actions. In Tables 3.1, 3.2, and 3.3, we’ve divided the class methods into general methods, filter methods, and render methods. Table 3.2 Filter methods Method
Use
add_filter
Adds a filter to a filter chain (typically not used directly)
after
Adds a filter to the after filter chain
before
Adds a filter to the before filter chain
skip_after
Skips a filter in the after filter chain
skip_before
Skips a filter in the before filter chain
skip_filter
Skips a filter previously added in a filter chain
normalize_filters!
Ensures that filter option values are arrays (typically not used directly)
From the Library of Shirong Chen
70
Chapter 3: Controllers
Table 3.3 Render methods Method
Use
default_layout
Resets layout options
layout
Sets the layout to be used or disables layouts when nil or false is given
render_options
Sets class-level layout options
template_roots
The root location of the templates the controller uses
template_roots=
Sets the root location of the templates the controller uses
3.2.1.2 Instance methods There are more API instance methods than class methods when it comes to Merb::AbstractController. Here we’ve divided them into general methods, URL methods, and view methods, as shown in Tables 3.4, 3.5, and 3.6. We’ve spoken before of using Merb outside web development, but the inclusion of URL methods in the abstract controller really pivots Merb use toward the web framework.
3.2.2 The Merb controller All Merb controllers inherit from the class Merb::Controller. This controller fully implements the web functionality for controllers. Therefore, it’s here that we’ll find the inclusion of request, parameter, and header methods. The API methods are broken down into class methods and instance methods; let’s go over these newly added methods.
3.2.2.1 Class methods The Merb::Controller class methods mainly serve to define what methods are available as actions and what formats are provided by those actions. Consequently, we’ve divided the methods introduced by the Merb::Controller into general and format methods, shown in Tables 3.7 and 3.8.
Table 3.4 General methods Method
Use
action_name
The action name associated with the controller instance
action_name=
Sets the action name associated with the controller instance
controller_name
Equivalent to the same class method
From the Library of Shirong Chen
3.2
The controller classes
71
Table 3.5 URL methods Method
Use
absolute_url
Returns the absolute URL of a route, including host and protocol
relative_url
Returns the relative URL of a route by route name or route parameters
url
Same as relative_url
Table 3.6 View methods Method
Use
body
Returns the body of the response being created
body=
Sets the body of the response being created
capture
Calls the capture method of the appropriate template engine
capture_erb
Captures an ERB template block, returning a string
catch_content
Returns the content “thrown” with a particular symbol
clear_content
Clears rendered content so another template can be rendered
concat
Calls the concat method on the appropriate template engine
concat_erb
Concats an ERB template block to the buffer
content_type
Gets the content type for the controller response
content_type=
Sets the content type for the controller response
partial
Used within templates for the embedding of templates
render
Renders a view with options, returning a full response with body
render_all
Calls render without any transformation
render_html
Calls render with the HTML MIME set
render_js
Calls render with the JS MIME set
render_json
Calls render with the JSON MIME set
render_text
Calls render with the text MIME set
render_xml
Calls render with the XML MIME set
render_yaml
Calls render with the YAML MIME set
template_roots
Equivalent to the class method with the same name
template_roots=
Equivalent to the class method with the same name
throw_content
Evaluates a template and stores it as a string for later use
thrown_content?
Checks whether content has been thrown
From the Library of Shirong Chen
72
Chapter 3: Controllers
Table 3.7 General methods Method
Use
callable_actions
Returns a set of all callable actions
hide_action
Hides an action, removing it from the set of callable actions
show_action
Adds an action to the set of callable actions
3.2.2.2 Instance methods The instance methods of Merb::Controller serve a number of purposes, most notably the need to make request and session data accessible to the action code. Other methods exist simply to aid in formatting. Tables 3.9 through 3.13 cover the most common API methods.
3.2.3 The application controller If you create a standard Merb application with merb-gen, you will find two files in app/controllers/. The first of these, Application, is meant to serve the parent class of all the application’s other controllers. This allows you to centralize shared code that would otherwise be found across your application’s controllers. For instance, here we have placed a filter that ensures that all web requests come from logged-in users: class Application < Merb::Controller before :ensure_authenticated end
Table 3.8 Format methods Method
Use
class_provided_formats
An array of formats the controller provides in responses
class_provided_formats=
Sets the array of formats the controller provides in responses
clear_provides
Clears the array of provided formats
does_not_provide
Asserts that an array is not provided
only_provides
Limits the array of provided formats to what is listed
provides
Appends an array of formats to the existing array of provided formats
reset_provides
Resets the array of provides
From the Library of Shirong Chen
3.2
The controller classes
73
Table 3.9 General methods Method
Use
headers
A hash containing the headers from the request
params
A mash containing the parameters from the request
status
The status code that will be sent
status=
Sets the status code for the response
request
The request object that got routed to the controller
rack_response
The rack response that will be delivered
redirect
Causes a redirect to a URL (note: will not exit from the action logic!)
message
The message that was included in the request
Table 3.10 Session methods Method
Use
cookies
The cookies from the request
delete_cookie
Deletes a cookie
set_cookie
Sets a cookie variable
session
Accesses the abstracted session store
Table 3.11 Format methods Method
Use
class_provided_formats
Proxy method for the class method
class_provided_formats=
Sets the proxy method for the class method
does_not_provide
Sets the formats not provided by an action
only_provides
Limits the formats provided by an action
provides
Describes the formats provided by an action
From the Library of Shirong Chen
74
Chapter 3: Controllers
Table 3.12 Escaping methods Method
Use
escape_xml
Escapes XML entities for use within XML
h
Escapes HTML entities for use within HTML
html_escape
Same as h
3.2.4 The exceptions controller The other controller generated as part of a Merb application is the exceptions controller. It is subclassed from Application and is used to craft error responses you may have already encountered during development. Note that it is common to extend or override the default responses. So, for instance, below we have created a new controller action that handles unauthenticated users. class Exceptions < Merb::Controller def unauthenticated render :format => :html end end
3.2.5 Other controllers As previously mentioned, Merb::Controller is not the only subclass of Merb:: AbstractController. In later chapters we’ll see the Merb::MailController and Table 3.13 Other methods Method
Use
nginx_send_file
Sends files through nginx
render_chunked
Renders the response in chunks
render_deferred
Mongrel-only method that defers rendering, allowing for other responses to be sent
render_then_call
Renders its parameter and then runs the block
run_later
Places a block in the worker queue
send_chunk
Sends a chunk
send_data
Sends binary data
send_file
Sends a file
stream_file
Streams a file
From the Library of Shirong Chen
3.3
Custom controller classes
75
Merb::PartController, each of which controls the flow of logic with actions without directly handling web requests.
3.3 Custom controller classes Now that we have described the various core controller classes as well as followed the pathway through which a request is handled, this is a good time to create our own controllers.
3.3.1 Controller location In the standard Merb application layout, all web controllers are located within the directory app/controllers/. We say all web controllers because the other controllers, MailController and PartController, tend to live elsewhere in other directories. However, if you’re using the flat layout, you’ll find that all of your controllers are kept in one file, application.rb. Also, in the case of the very flat layout, your controllers basically have nowhere else to live but as part of a single file. Note that both the flat and very flat layouts are generated with a custom controller subclassed directly from Merb::Controller instead of Application. This seems to be based on the assumption that you’ll be making an application small enough that it does not need a parent class for all controllers. However, don’t let that restrict you—if you’re using the flatter layouts and need Application or Exception, by all means, code them in. Even though it’s extremely rare to see someone deviate from the default locations, you can change the location of your controllers if you want. To do this, you can define a framework hash for Merb::Config inside the file config/framework.rb. This alters the default framework locations at Merb boot-up, but make sure you mention all locations you want to be set, because framework.rb doesn’t merge with the default settings. The following snippet shows, among other things, how to set the location of controllers to controllers/ as opposed to app/controllers: Merb::Config[:framework] = { :controller => Merb.root / "controllers", :view => Merb.root / "views", :model => Merb.root / "models", :lib => Merb.root / "lib", :public => [Merb.root / "public", nil], :router => [Merb.root / "config", "router.rb"] }
From the Library of Shirong Chen
76
Chapter 3: Controllers
To check that this has had an effect or just to find out where your application thinks controllers should be, you can always fire up interactive Merb and have it evaluate the following: $ merb -i > Merb.dir_for(:controller) => "/home/foysavas/src/the-merb-way/code/framework_changes/ controllers"
3.3.2 Naming controllers By convention, the filename of a controller file should be the snake case of the contained controller’s class name. For example, a controller named OrderTransactions would be contained in a file named order_transactions.rb. This isn’t strictly necessary, but it’s good practice and a convention that others can recognize. As long as your controllers are located within the controller directory or one of its subdirectories, they will be loaded at boot. Feel free to name each controller whatever you’d like, but keep in mind that the names of controllers could collide with other class names in the global namespace. This specifically becomes a problem with models. Consequently, the convention is to name controllers that have corresponding models in the plural form. For example, you’ll often find a controller named Users when there’s a model named User. If there is no corresponding model, don’t worry; just name your controller whatever makes the most sense. Welcome and Home are good examples of names for a controller that would handle the landing page of your application.
3.3.3 Organizing controller methods The organization of a controller’s methods is critical to how a controller responds to requests. In general, you should define one public instance method for each type of request a controller will handle. If you need other methods that won’t handle requests directly, you should leave them for near the end of your controller and mark them either private or protected. Both private and protected, in the context of Merb controllers, have the same effect, as it would be odd to have two Merb controller instances working together during a single request. Typically, you use private or protected methods in your controller classes because they allow you to • Share a nonaction method among controller actions • Make methods available to all controller subclasses • Increase the readability of code within your controller
From the Library of Shirong Chen
3.3
Custom controller classes
77
3.3.3.1 Sharing a nonaction method In this fabricated example we use a private method as a means of accessing data: class ColorTriad < Merb::Controller def index get_all_colors.join end def show get_all_colors[params[:id]] end private def get_all_colors ['red','blue','green'] end end
In our model-less world of controller-only applications this makes sense. However, more realistically, such a method would most likely encapsulate several lines of data, retrieving code that would otherwise appear in multiple controller actions. A perfect example of this is the case of the show and edit methods of a RESTful resource.
3.3.3.2 Making methods available to subclasses Sometimes you want to make a method available to more than one controller. The most typical way of doing this is to make it a private method within the Application class: class Application < Merb::Controller private def users_in_groups { 'me' => ['Merb Developers','Likes Durian'], 'you' => ['Merb Developers','Durian Stinks!'] } end end class Users < Application def index users_in_groups.map do |u, g| u + ' is in ' + g.join(',') end.join("\n") end end
From the Library of Shirong Chen
78
Chapter 3: Controllers
class Groups < Application def index groups = [] users_in_groups.each do |u, g| groups[:g] { :login }
Sometimes you want to have filters applied only in the case of more complex conditions. The :if and :unless options allow you to do this with either a method name or Proc: class RoomService < Merb::Controller before :automate_response, :if => Proc.new { Time.now.hour < 6 } def index "Room service - may I help you?" end private def automate_response throw :halt, "Sorry, room service is unavailable at the moment." end end
You can also pass parameters to filters by using :with, but when doing so you’ll have to make sure that the arity of the filter matches the size of the :with array: before :has_role, :with => ['Admin']
3.4.4 Skipping filters On occasion you’ll want to skip a filter that was defined in a superclass. To do this, you can use the skip_before and skip_after class methods from AbstractController: class Application < Merb::Controller before :login_required private def login_required unless session[:user_id] throw :halt, Proc.new { |c| c.redirect url(:controller => 'sessions', : method => 'new') } end end
From the Library of Shirong Chen
84
Chapter 3: Controllers
end class Sessions < Application skip_before :login_required, :only => %w{ new create } end
3.5 Redirects There are many cases when a user has sent a request that should ultimately get redirected. You can make this happen by using the controller method redirect. The redirect method accepts a string for a URL as its first parameter and also takes an optional hash. This optional hash may include a Boolean setting of :permanent and a :message to pass on: redirect "/login" redirect "http://www.merbivore.com", :permanent => true redirect "/", :message => 'Please, sign up or log in to continue."
3.5.1 A redirect caveat One important point to remember when using redirects is that they do not stop the execution of the controller action, and whatever code is left to evaluate will run before the redirect is sent. The controller action, however, will not get the chance to return a value. The following example should make this clear: Merb::Router.prepare do to(:controller => 'recruiters_dilemma') do match('/').to(:action =>'index') match('/show_prize').to(:action => 'show_prize') match('/apologize').to(:action => 'apologize') end end class RecruitersDilemma < Merb::Controller def index if params[:people].to_i >= 10 redirect '/show_prize' else 'How many people have you recruited? Append it as a number after "?people=" to this URL.' end end
From the Library of Shirong Chen
3.5
Redirects
85
def show_prize redirect '/apologize', :permanent => true Merb.logger.warn "Here comes another winner!" "Your brand-new Ferrari F355!" end def apologize "Our apologies." end end
In the previous example, "Here comes another winner!" is logged, but because of the redirect, which is essentially a change in management for the final response, none of us gets our F355. This example is harmless enough for both user and developer, but you should be cautious when using redirects and make sure that unnecessary business logic does not run. Another thing to note in this example is the use of :permanent in the second redirect. This effectively changes the status code of the response from 301 to 302 and from the browser’s perspective assures that '/show_prize' should always point to '/apologize'.
3.5.2 Redirects after POST requests The most common use of redirects in practice is to redirect users after they have posted content. You may not be familiar enough with Merb to completely understand the following example, but here a user has posted what looks like a new entry of some sort, and if it saves without error, the user is redirected to the list of entries: class Entries < Application # ... def create @entry = params[:entry] if @entry.save redirect '/entries' else render :new end end # ... end
From the Library of Shirong Chen
86
Chapter 3: Controllers
3.5.3 Redirecting in before filters At other times, controllers redirect users before the action ever starts. This most likely occurs because a particular resource doesn’t exist or because a user has yet to log in. Before filters are a great place for this kind of logic because these cases tend to apply to multiple controller actions and are simplified logistically by gaining access to the throwing of halts. Let’s take a look at an example: Merb::Router.prepare do to(:controller => 'users_names') do match('/').to(:action =>'index') match('/:username').to(:action => 'show') end end DATA = { :users => { :foysavas => 'Foy Savas', :sophiachou => 'Sophia Chou' } }
class UsersNames < Merb::Controller before :get_by_username, :only => 'show' def index message || "Welcome" end def show "#{params[:username]} => #{@user}" end private def get_by_username if (@user = DATA[:users][params[:username].to_sym]).nil? throw :halt, Proc.new{ redirect '/', :message => "#{params[:username]} not found" } end end end
An important takeaway from this example is how the redirect was accomplished. Before filters are able to catch thrown halts that can also take Procs. These Procs are
From the Library of Shirong Chen
3.6
Exceptions
87
evaluated using instance_eval. By putting a redirect in this Proc, we are able to redirect the controller without having to worry about more of the controller code being evaluated. Though it may seem redundant to mention, throw :halt does not work anywhere but inside the before filter chain, so don’t try to use it anywhere else or you’ll just get an uncaught exception and an error that looks like this: ˜ uncaught throw ‘halt' in thread 0xd6bad0 - (ThreadError)
3.6 Exceptions If you’re looking for something similar to throw :halt that you can use outside before filters, raising exceptions may be your answer. By raising particular controller exceptions, you can halt controller execution midway and internally reroute the request to the Exceptions controller. There’s one important thing to note, though: Unlike redirects, using Exceptions means it’s the end of the line for your user and an error page will be displayed. Yes, you’ll be able to make this error page pleasing and include a cute mascot, but ultimately it is an error page, so if you’re going to use it, make sure that’s what you intend and not a more subtle redirect.
3.6.1 Raising an exception Many exceptions have already been defined for you within the Merb core. Let’s go through an example using one of the predefined exceptions to get a basic feel for how they are used: Merb::Router.prepare do match('/').to(:controller => 'exceptional', :action =>'index') end class Exceptional < Merb::Controller def index raise NotFound end end
Here, when we request the index, a Not Found error appears along with a 404 status code in the response.
3.6.2 Controller exceptions When you raise an exception somewhere in your controller code, it is rescued by the dispatcher and redispatched to be handled as a controller exception. These exceptions
From the Library of Shirong Chen
88
Chapter 3: Controllers
are defined in the module Merb::ControllerExceptions and offer an exception for each of the W3C standard HTTP status codes. If the error raised is not among these existing controller exceptions, Merb interprets it as an internal exception and uses the class InternalServerError to handle it. This is the error page you’ve seen in the past when something has gone wrong with your code.
3.6.3 The exceptions controller The exceptions controller is one of the core controller classes we talked about earlier. Its purpose is to translate an exception into a response. Merb handles the routing behind the scenes, but what’s important to know is that each exception class name is snake-cased and then called as an action method name in Exceptions. Below we see a custom controller exception that, with the help of the exceptions controller, responds with a string. Merb::Router.prepare do match('/').to(:controller => 'sad_hacking', :action =>'index') match('/root').to(:controller => 'sad_hacking', :action =>'root') end class SadHacking < Merb::Controller def index "hack me" end def root raise BadHacker end end class BadHacker < Merb::ControllerExceptions::Unauthorized; end class Application < Merb::Controller ; end class Exceptions < Application def bad_hacker "that was pathetic..." end end
If you request /root, the BadHacker exception will be handled in the same way as an unauthorized user. The only difference, of course, is the custom response found in the exceptions controller.
From the Library of Shirong Chen
3.7
Rendering templates
89
3.7 Rendering templates Rendering templates is one of the most common actions you’ll find at the end of a controller. Consequently, there are a number of methods found in the Merb::Controller class that deserve our consideration now before the chapter on views (Chapter 4). We’ll go over each of these methods, but before we do, we’ll need to get comfortable with how Merb brings together controllers and templates.
3.7.1 How templates are compiled Templates typically exist as files stored in app/views, all of which Merb opens up and inlines into a template method. These template methods are then accessible by original filename to any of the subclasses of Merb::AbstractController. Typically, a controller associates itself with a particular path, and then the action that calls render appends its name as a file followed by a response format extension and then a template format. For example, the action Users#index associates itself by default with the template at app/views/users/index.html.erb. The .html is there since HTML is the default format that Merb uses for responses. The .erb is for ERB, which is the default template format. ERB is a versatile template type that has the potential to render HTML, XML, JSON, or really any textual format. However, there are other template engines that may be more suitable for particular responses. These alternatives include Haml, which excels in structuring HTML and XML with extreme brevity. For now, though, since we’re currently considering templates only in the context of controller logic and not in that of the view, we’ll use only ERB. In fact, in order to keep with our desire to produce single-file Merb applications, we’re going to do something completely unconventional: We’re going to include our templates as virtual files. You will probably never do such a thing, but getting a good feel for how templates are compiled through imitation will definitely demystify what otherwise may seem like magic. Let’s start off with a simple example: Merb::Config.use { |c| c[:framework] ] }, c[:session_store] c[:exception_details] }
= { :public => [Merb.root / "public", nil = 'none', = true
Merb::Router.prepare do match('/').to(:controller => 'weird', :action =>'index') end
From the Library of Shirong Chen
90
Chapter 3: Controllers
Merb::BootLoader.after_app_loads do vf = VirtualFile.new("hi ",'app/views/weird/ index.html.erb') Merb::Template.inline_template(vf) end class Weird < Merb::Controller def index @var = 'friend' render end end
If you run the code above as a single-file application and check http://localhost: 4000, you’ll find a friendly 'hi'. You’ll recognize that pulling this off wasn’t as easy as it was in the past. The first thing we did was add a Merb::BootLoader. after_app_loads block. As we saw in Chapter 1, this block runs after our application code has been loaded. Here we use it to first create a fake template using a virtual file and then inline that virtual file using inline_template. Notice that the virtual file takes two parameters for initialization. The first is what would have been the actual content of the file. The second is a fake file path. Here we’ve matched it to what the Weird#index method would naturally associate itself with. Now let’s take a look at the content of the virtual file. You’ll notice some text, some odd bracketing, and a class method inside. We’ll go over these odd brackets in the next chapter, but suffice it to say that they designate sections of an ERB template that should be interpreted in Ruby, and in this case also appended to the template. But why a class variable? Remember that all templates are included as methods of the abstract controller. Consequently, class methods from the controller instance are also available to the template itself.
Speed through common sense or just contentious? Merb has always emphasized speed, but oddly enough it achieves this most often through common sense. We saw that Merb inlines its templates and includes them as methods. Not only is this fast, but given the nature of template variables, it also makes good sense. Not everyone is pleased, though. Many believe that Merb’s inlining of templates makes it only quasi-MVC because it effectively squeezes together the controllers and views. In this respect, Rails 3 will notably not stay with the Merb way of doing things.
From the Library of Shirong Chen
3.7
Rendering templates
91
3.7.2 Basic rendering In order to render a template, we use the controller method render. Note that render isn’t some magical method that takes control of your action. Instead it simply evaluates a template returning a string. Thus, it must be the final evaluation step on your controller action to have any effect. In the following modification of our first example, the rendered template is mistakenly not the final step of the action, and 'bye' ends up being the response instead: Merb::BootLoader.after_app_loads do vf = VirtualFile.new( "hi ", 'app/views/weird/index.html.erb') Merb::Template.inline_template(vf) end class Weird < Merb::Controller def index @var = 'friend' render 'bye' end end
The render method can take various parameters. The simplest of these is a string. When you render a string, the render method embeds the string within the layout (more on this later) or, if no layout is available, simply returns the string itself. Here is an example: class Example < Merb::Controller def index render "hello world" end end
3.7.3 Template The render method may alternatively take a symbol as one of its parameters. This switches the template to be rendered from the action name to a stringified version of the symbol. This is most commonly used to render a form when form submission has failed. We’ll see an example of this in Chapter 6 on helpers, but for now, here’s a simpler example based upon the first. Note how the template’s name has been changed. Merb::BootLoader.after_app_loads do vf = VirtualFile.new( "hi ",
From the Library of Shirong Chen
92
Chapter 3: Controllers
'app/views/weird/not_index.html.erb') Merb::Template.inline_template(vf) end class Weird < Merb::Controller def index @var = 'friend' render :not_index end end
Another way of specifying the template is to pass in an optional hash with :template with the template’s name as its value: class Weird < Merb::Controller def index @var = 'friend' render :template => :not_index end end
3.7.4 Formats We can also specify the format with which an action will be rendered. By default this format is HTML, but we can override this by using the hash key :format. Be aware that this requires that a template with that filename exists. Merb::BootLoader.after_app_loads do vf = VirtualFile.new( "{'response': 'hi '}", 'app/views/weird/not_index.json.erb') Merb::Template.inline_template(vf) end class Weird < Merb::Controller def index @var = 'friend' render :format => :json end end
We can accomplish the same thing in two other ways. The first is setting the content_type, which may have been automatically set by the router from the value for
the route parameter format: class Weird < Merb::Controller def index
From the Library of Shirong Chen
3.7
Rendering templates
93
@var = 'friend' format_type = :json render end end
The second is using the method render_json. Merb adds a render_ prefixed method for each of the MIME types to which it can respond. Here we use it in our action: class Weird < Merb::Controller def index @var = 'friend' render_json end end
Occasionally there are several ways to do the same thing. Our recommendation is to stick with the simplest. Of course, deciding what that is, is a matter of opinion. We can also specify the formats provided by a controller when we want to be explicit. These can then be automatically specified as the formats in request parameters. The two methods that open up this possibility are provides and does_not_provide. Both of these are available as class and instance methods. The example below demonstrates both methods. Merb::Router.prepare do default_routes end Merb::BootLoader.after_app_loads do templates = [ VirtualFile.new( "{'response': 'hi '}", 'app/views/weird/index.json.erb'), VirtualFile.new( "hi ", 'app/views/weird/index.html.erb'), VirtualFile.new( "Only in HTML", 'app/views/weird/extra.html.erb') ] templates.each do |t| Merb::Template.inline_template(t) end
From the Library of Shirong Chen
94
Chapter 3: Controllers
end class Weird < Merb::Controller provides :html, :json def index @var = 'friend' render end def extra does_not_provide :json render end end
3.7.5 Status You can also set the status code used in the response through render by setting :status to some integer. This, however, doesn’t actually affect the render itself but instead only incidentally sets the value of status used later by the Merb server in crafting the headers for the response. class Missing < Merb::Controller def index @var = 'friend - are you lost?' render :status => 404 end end
3.7.6 Layout A layout is a parent template in which a rendered template can be embedded. Layouts typically reside in app/views/layouts/. The default layout in a controller without a specified layout is app/views/layout/:controller.html.*, where :controller is the controller’s name in snake case. If that is not found, the controller looks for app/views/layouts/application.html.*. Either way, this also requires that we indicate where the render content should be placed. To do this we must use the method catch_content :for_layout. Below we put all of this together to produce the output "START hi friend FINISH": Merb::BootLoader.after_app_loads do vf = VirtualFile.new(
From the Library of Shirong Chen
3.7
Rendering templates
95
"START FINISH", 'app/views/layout/weird.html.erb') Merb::Template.inline_template(vf) vf = VirtualFile.new( "hi ", 'app/views/weird/index.html.erb') Merb::Template.inline_template(vf) end class Weird < Merb::Controller def index @var = 'friend' render end end
We can specify an alternative layout in various ways. We can do this controller-wide using the layout method. Alternatively, we can set it as the value of the :layout key in the render hash. Note that if we use the value nil, no layout will be used and the template will be rendered alone: class Example < Merb::Controller layout :elsewhere def index render :layout => nil end end
3.7.7 display An alternative way to render a template is to use the method display. In scenarios where there is an object that essentially forms the entirety of the response, this method may be a better fit than render, as it falls back on serialization of that object when an appropriate template isn’t found. We’ll use this effectively to handle multiple MIME templates, including XML and JSON, in Chapter 6. For now, the following contrived example shows the gist of the intent: Merb::BootLoader.after_app_loads do vf = VirtualFile.new( "hi", 'app/views/example/index.html.erb') Merb::Template.inline_template(vf) end
From the Library of Shirong Chen
96
Chapter 3: Controllers
class Example < Merb::Controller provides :html, :json, :xml def index obj = {:first => [1,2,3]} diplay obj end end
3.7.8 render_chunked Occasionally we may want to send data chunked to save on memory usage. This would make sense especially if we were going to send over a large log file that we were reading from a private location. In this example taken from the API, we pass the method render_chunked a block that contains looped calls to send_chunk: def stream prefix = '
' suffix = "
\r\n" render_chunked do IO.popen("cat /tmp/test.log") do |io| done = false until done sleep 0.3 line = io.gets.chomp if line == 'EOF' done = true else send_chunk(prefix + line + suffix) end end end end
3.7.9 render_deferred If we are using the Mongrel web server, then another method available to us is render_deferred. When we use it in place of render, we can pass in a block that is executed before the final rendering. Nonetheless, the Merb thread is not locked during its evaluation of the block and is capable of handling other requests. class Example < Merb::Controller def index render_deferred do
From the Library of Shirong Chen
3.9 Sending and streaming
97
sleep 10 end end end
3.7.10 render_then_call Another method available to us is render_then_call. This method renders normally, but the block it is given is called after the response is sent. Thus, this block is evaluated outside the request-to-response cycle, making it a great place to put responseindependent, time-intensive instructions: class Example < Merb::Controller def index render_then_call "hi there" do sleep 10 end end end
We hope that sleeping on the job isn’t actually part of your application’s specifications.
3.8 run_later A better way of calling a block later on is to use the web-server-agnostic run_later method. This method places the block into the worker queue and has it run at some point after the response is handled. We can use this extensively once again for timeintensive instructions that are not necessary for the controller response. Also, because run_later is essentially just a means of pushing blocks into a queue, we can have as many of them as we like. class Example < Merb::Controller def index run_later do sleep 10 end "hi" end
3.9 Sending and streaming Sending and streaming allow us to programmatically deliver data from inside controller actions. This may seem odd at first when you could simply serve a static resource, but there are scenarios when you’ll find this helpful. For sending, this may be when you want to
From the Library of Shirong Chen
98
Chapter 3: Controllers
protect a file behind filters that among other things authorizes users. In the case of streaming, it may be because files are large and serving them a chunk at a time saves memory.
3.9.1 Sending files The method for sending a file is simply send_file. It requires only a single parameter, the path to the file, but you can explicitly set the filename, disposition, and file type through an options hash. Below we send a file only if the request comes from an authenticated user. class Files < Merb::Controller before :ensure_authenticated def book send_file('/data/books/the_merb_way.pdf', :filename => 'understanding_merb.pdf') end end
3.9.2 Streaming files Streaming is the process of sending information to the client in chunks, which saves on memory when we’re dealing with large files. The Merb method stream_file offers this benefit. In the example below, we stream data from Amazon S3 containing a user’s video intro stream_file. class Users < Merb::Controller before :find_user, :only => %w{ video_intro } def video_intro stream_file({ :filename => filename, :type => content_type, :content_length => content_length }) do |response| AWS::S3::S3Object.stream(@user.login, bucket_name) do |chunk| response.write chunk end end end # ... end
From the Library of Shirong Chen
3.10
Conclusion
99
3.10 Conclusion Controllers are an essential part of any Merb application. In this chapter we exhibited their central importance by almost exclusively coding all the examples using only controllers. Our tour of features such as filters, exceptions, and rendering should already enable many application developers to get a head start on building Merb applications. As we move on to the next chapter on views, remember that controllers and views are internally tightly coupled within the framework; the next chapter is more a discussion of Merb’s interaction with templating engines than anything else.
From the Library of Shirong Chen
This page intentionally left blank
From the Library of Shirong Chen
C HAPTER 4
Views
While web services may need to serialize data only when sending responses, users typically interpret websites visually through HTML. Consequently, in order to better separate the formatting of data from its composition, the MVC paradigm pushes us toward the convenience of views. Within a Merb application, these concretely translate to using templates from which application responses are built. Not all templates are formatted in the same way, though, and some are better suited than others for particular work. Consequently, Merb has since its beginnings been able to hook into different templating engines. Working off the Erubis and Haml engines, Merb enables the two most common template formats, ERB and Haml. We will cover both of these in this chapter as well as more specifically how to use them within Merb itself. As we’ll discover, Merb also enhances these templates in various ways, sometimes fundamentally as in the case of the BlockAwareEnhancer but most of the time more mildly through the inclusion of helpers and local variables. Ultimately, views are a critical part of nearly every web application, but the Merb way of building applications has always been to minimize the amount of logic within them. While it’s easy to start employing Ruby to its full extent within views, we strongly advise considerable restraint while crafting views. Try pushing off relevant logic to helpers, controllers, or, better yet, models.
4.1 ERB ERB, more properly known as eRuby, is the default templating system used by Merb. Because it does not make any requirements on the structure of the templates, ERB is amazingly versatile and can produce any textual format you may need for output, including, among other formats, HTML and XML. It is worthwhile to mention that nearly all the files produced by merb-gen are created using ERB templates. 101 From the Library of Shirong Chen
102
Chapter 4: Views
Merb compiles ERB using the Erubis templating engine, an extremely fast implementation of ERB. You can use Erubis by itself outside Merb, and in order to introduce the basics of ERB, we’ll do just that.
4.1.1 Basic delimiters The are two principal delimiters used within ERB: and . If you have experience with JSP or ASP, you’ll find that their use is identical. Of the two, the first executes code, and the second executes and then inserts the result into the template ouput. The following template exhibits both:
We can excute the file using the command erubis. Here’s the result: $ erubis hello.erb Hey you!
We are also able to pass in values for the variables from the command line using -c: $ erubis -c "@name = 'Sophia'" hello.erb Hello Sophia!
4.1.2 Removing whitespace Unlike other ERB implementations, Erubis automatically trims whitespace from its execution-only delimiters. Output delimiters, however, include both leading and trailing whitespace. If necessary you can eliminate trailing newlines using the modified delimiter . This may not matter for an HTML file, but if you’re using ERB to format plain text or code, you’ll definitely appreciate its existence. As an example, the following script outputs the numbers 1 through 10 all on the same line. Note that we’ve included a final line break to compensate for its final suppression.
From the Library of Shirong Chen
4.1
ERB
103
4.1.3 Comments If you need to quickly cancel the effect of a line of ERB in a noncommittal way, the pound sign will do the trick. Because ERB interprets text delimited by as Ruby, squeezing in a # effectively puts a stop to both execution-only and output-delimited code:
4.1.4 Merb’s block-aware enhancer Unfortunately, standard ERB lacks the ability to execute multiline Ruby code split among multiple delimiters. In other words, the following simply will not work:
This may not seem problematic at first (who would ever need to do this?), but template helper methods often take blocks that sometimes cannot reasonably fit in one delimiter. Here’s a somewhat less contrived example of a helper using a block just to get the point across. Note that it does not work in standard ERB.
The work-around used by Rails (and most likely to be abandoned in the merge) has been to overload the use of execution-only delimiters through the inclusion of code in helper methods. This code first checks if the template being used is ERB or not and then forcefully outputs the result after interpreting the block. The Merb team turned away from this solution unsatisfied, and Yehuda Katz instead crafted an Erubis enhancer as an alternative solution. The BlockAwareEnhancer found in the Merb core does precisely what it says it does, enhancing ERB so that it can spot and use multiline blocks. This does come with one slight inconvenience, though: The ending line of a block must use a special tail character on its directive, . With this in mind, let’s rewrite the previous nonworking example to make it suitable for use in Merb:
Note that no alteration of the helper method was needed. Let’s take a look at the source code behind the enhancer to get a better feel for how it was accomplished: module Erubis module BlockAwareEnhancer # :api: private def add_preamble(src) src not good for your health. Well, there are a couple of ways of pulling it off. The first is to simply include the tags as part of the unsanitized content of some element. For instance, here we set the lines above as the value of a span: %span <strong>Don't forget: inline styles are <em>not good for your health. The second is to use a filter as a work-around. We use a plain filter below, but an ERB filter would work equally well. :plain <strong>Don't forget: inline styles are <em>not good for your health.
4.3 Merb view templates The view templates of a Merb application are stored within the directory app/views/. Conventionally, each controller associates with a subdirectory, and each of its actions
From the Library of Shirong Chen
110
Chapter 4: Views
is handled by templates contained within. So, for example, the controller Users has its view templates located within app/views/users/, which may contain files like index.html.haml or show.html.erb. Such templates are used when we call the method render or display inside of the controller action: class Users < Application def index @users = User.all render end def show(id) @user = User.get!(id) display @user end end
Remembering that the controller and view code are tightly coupled within Merb, we can find the private method within the controller render mixin that when called by render looks for these template files. It’s called _template_for: module Merb::RenderMixin::ClassMethods def _template_for(context, content_type, controller=nil, template=nil, locals=[]) tmp = self.class._templates_for[[context, content_type, controller, template, locals]] return tmp if tmp template_method, template_location = nil, nil if template.is_a?(String) && template =˜ %r{ˆ/} template_location = self._absolute_template_location( template, content_type) return [ _template_method_for(template_location, locals), template_location ] end self.class._template_roots.reverse_each do |root, template_meth| # :template => "foo/bar.html" where
From the Library of Shirong Chen
4.4
Partials
111
# root / "foo/bar.html.*" exists if template template_location = root / self.send( template_meth, template, content_type, nil) # :layout => "foo" where # root / "layouts" / "#{controller}.html.*" exists else template_location = root / self.send( template_meth, context, content_type, controller) end break if template_method = _template_method_for( template_location.to_s, locals) end # template_location is a Pathname ret = [template_method, template_location.to_s] unless Merb::Config[:reload_templates] self.class._templates_for[[context, content_type, controller, template, locals]] = ret end ret end end
Note that this code first checks from a memoized list to see if a matching template has previously been found. If not, it proceeds to look for one in each of the template roots (of which there is typically only a full path to "app/views"), finding what it can. You may also spot the use of the pathname method /. Many of the methods shown above have been defined elsewhere, and the interested reader can discover a layering of AbstractController and Controller methods in that mixin method.
4.4 Partials Partials are subtemplates used within view templates. To incorporate them, we can use the method partial within a view. Here we do so in both ERB and then in Haml: # app/views/users/index.html.erb u %>
From the Library of Shirong Chen
112
Chapter 4: Views
# app/views/users/index.html.haml - @users.each do |u| = partial 'users/preview', :user => u
The partial method takes as a first argument the path to the template. For example, above the path is 'users/preview'. There is one subtlety, however: Actual partial template files begin with an underscore. Thus, the path of the preview template is actually app/views/user/_preview.html.haml, possibly varying in template extension. The hash that forms the second and optional parameter on partial represents the local variables that are inserted and available to the partial.
4.5 Conclusion It’s our recommendation that all Ruby web developers become comfortable with both ERB and Haml. While ERB’s versatility makes it a tool capable of every job, Haml’s brevity outshines ERB when targeting HTML and XML. As far as Merb is concerned, either works well, and as we’ve seen through the creation of the Erubis BlockAwareEnhancer, little of the core’s rendering code has to be aware of which templating engine is being used.
From the Library of Shirong Chen
C HAPTER 5
Models
Models are related data and algorithms that respectively represent the properties and behaviors of domain objects used within an application. Models are used in nearly every decently complex object-oriented application because they organize application data in object form. Within the scope of a typical Merb application, examples of modeled objects may include users, posts, entries, or comments. These objects are most often persisted in a database through the use of an Object Relational Mapper, or ORM. As agnosticism has been central to Merb’s design, application developers are free to integrate whatever Ruby ORM they may need. Nonetheless, the default ORM included as part of the standard Merb stack is DataMapper, an ActiveRecord-like ORM that aims to push the boundaries of object-data interaction even more significantly toward the object side. Given its acceptance as part of the standard Merb stack, we will cover the use of only DataMapper in this chapter. However, don’t let that stop you from using ActiveRecord, Sequel, or any of the other options, since each of these ORMs is capable of creating, retrieving, updating, and deleting persisted data and is fully supported by the Merb core.
5.1 Configuration Prior to being able to use the DataMapper ORM within your Merb application, you will have to make sure that you have done three things: • Included the DataMapper dependencies within config/dependencies.rb • Selected DataMapper as the ORM in init.rb • Configured your database connection in config/database.yml
113 From the Library of Shirong Chen
114
Chapter 5: Models
However, if you used merb-gen to generate a standard application and have SQLite3 installed, then you’ll find that all of these are already covered for you. If your application was generated in some other manner, you may want to see Chapter 1 to figure out how your configuration files should be laid out. Other standard dependencies The standard Merb generator inserts several other DataMapper dependencies into the file config/dependencies.rb. These commonly used DataMapper plugins provide functionality such as migrations, automatic timestamping, validations, and record aggregation. You are free to comment out or remove any of these dependencies, but as we’ll see through examples, even the most basic application models tend to want them to be available.
DataMapper works with several different database back ends. Support for MySQL, PostgreSQL, and SQLite3 are all included via the DataMapper core. If you’re using another database application, you may be able to find an appropriate adapter in the dm-more or dm-adapters gems. Alternatively, if your computer has the memory for it, there’s a DataMapper adapter that will store your model records completely in memory. Not just databases When you use DataMapper, you aren’t limited to interacting only with conventional databases. Instead, the DataMapper concept of a repository applies equally well to just about any data source composed of records. This includes numerous web service APIs where CRUD-like operations are available. A great example of such an adapter is the Salesforce adapter Yehuda Katz has written. In just under 300 lines of code, the adapter provides full DataMapper interaction via the Salesforce API. You’ll find that these adapters can also be used to augment models through secondary repositories, which is commonly the case when using Bernerd Schaefer’s full-text-search Ferret adapter.
Repository configuration is done in a YAML file named config/database.yml. The standardly generated config/database.yml file is broken into four environments—development, test, production, and rake—corresponding to the environments that are standardly used by a Merb application. If you have another environment—for instance, a staging environment—it, too, can be listed in this file.
From the Library of Shirong Chen
5.2
Model classes
115
Typically there is overlap in the configuration of environments, so YAML node anchors and aliases are used to reduce redundancy. What’s a YAML node? YAML nodes are named blocks of content in a YAML file. You can set up an anchor to the content of a node by using an ampersand and word after the naming of the node. Below we connect the content of development with the anchor &default. development: &default adapter: mysql database: example_development We can reference this anchor by using the alias *default later on. If we use this alias as the value on a merge key, :groups}"} - reset_cycle(:groups)
From the Library of Shirong Chen
6.5
Tag helpers
177
Here’s the source behind the cycle method: def cycle(*values) options = extract_options_from_args!(values) || {} key = (options[:name] || :default).to_sym (@cycle_positions ||= {})[key] ||= {:position => -1, :values => values} unless values == @cycle_positions[key][:values] @cycle_positions[key] = {:position => -1, :values => values} end current = @cycle_positions[key][:position] @cycle_positions[key][:position] = current + 1 values.at( (current + 1) % values.length).to_s end
Note that the default cycle name is :default and that it is extracted from the values using the method extract_options_from_args!, which is an extension the Merb core makes to Kernel: class Kernel # :api: public def extract_options_from_args!(args) args.pop if Hash === args.last end end
6.5 Tag helpers The tag helpers, of which there are only four, are fundamental to the creation of HTML and XML entities. Let’s go through the code that pulls these together: module Merb module Helpers module Tag def tag(name, contents = nil, attrs = {}, &block) attrs, contents = contents, nil if contents.is_a?(Hash) contents = capture(&block) if block_given? open_tag(name, attrs) + contents.to_s + close_tag(name) end
From the Library of Shirong Chen
178
Chapter 6: Helpers def open_tag(name, attrs = nil) "" end # Creates a closing tag def close_tag(name) "" end def self_closing_tag(name, attrs = nil) "" end
end end end module Merb::GlobalHelpers include Merb::Helpers::Tag end
We find that the tag helpers, like other helpers, are contained within a module and then included within the module Merb::GlobalHelpers. Remember that this module is included within the Merb::Controller, and consequently these methods are available to all controllers and all views. The first method, tag, can take three arguments: name, contents, and attrs. Thus we can create HTML entities as follows: tag("div","Hello There", :class => "message")
It also takes an optional block that we can use to place in nested entities: tag(:h3) do tag(:div,"Hello There", :class => "message") end
Most of the hard work is done through the other helper methods and to_ html_attributes. Chances are you will not use these helpers within templates themselves, because their real target is within other helpers, allowing for the construction of HTML entities in an abstract, template-agnostic way. In the next section, we’ll see just that.
From the Library of Shirong Chen
6.6
Form helpers
179
6.6 Form helpers The design of the form helpers is more complex than that of any of the other helpers described in this chapter. With that in mind, we will not cover the API of helpers entirely in this section and aim only to fundamentally understand the design of helpers by picking apart its quintessential parts. Let’s start off with the underlying builder class upon which the actual helpers rely.
6.6.1 Builders The Form::Builder module holds module Merb::Helpers::Form::Builder class Base include Merb::Helpers::Tag def initialize(obj, name, origin) @obj, @origin = obj, origin @name = name || @obj.class.name.snake_case. split("/").last end
Note the class Base. It has access to the tag helpers previously described, so we can expect them to be littered throughout the rest of the code. It’s also the class that will be used under the hood by the form helpers themselves. As such, it must maintain three instance variables, two of which may simply end up being nil. The first of these, obj, is meant as a reference to the model object with which the form may be associated and may be nil. The second, name, holds the name of the form context, which again is related to the model object and most of the time derived from it. This parameter can also be nil without causing issues. Finally, the parameter origin links us back to the actual form helper making use of the base. This parameter is strictly necessary. def form(attrs = {}, &blk) captured = @origin.capture(&blk) fake_method_tag = process_form_attrs(attrs) tag(:form, fake_method_tag + captured, attrs) end def fieldset(attrs, &blk) legend = (l_attr = attrs.delete(:legend)) ? tag(:legend, l_attr) : "" tag(:fieldset, legend + @origin.capture(&blk), attrs) end
From the Library of Shirong Chen
180
Chapter 6: Helpers
The two methods form and fieldset are used indirectly in the production of form and fieldset elements. We have grouped them together for you because they both make use of blocks passed to them. It’s worthwhile to point out that the capture method is employed in handling these blocks, which ultimately means that they will be handled by the template engine. You may also have spotted the use of process_form_attrs: private def process_form_attrs(attrs) method = attrs[:method] attrs[:method] = :post unless attrs[:method] == :get method ||= (@obj.respond_to?(:new_record?) &&
[email protected]_record?) || (@obj.respond_to?(:new?) &&
[email protected]?) ? :put : :post attrs[:enctype] = "multipart/form-data" if attrs.delete(:multipart) || @multipart method == :post || method == :get ? "" : fake_out_method(attrs, method) end def fake_out_method(attrs, method) self_closing_tag(:input, :type => "hidden", :name => "_method", :value => method) end
The processing of form attributes does three things. First, it checks to see if we’ve set the form to use the GET method. If so, it leaves it alone. Otherwise it will use POST. In general, all forms should use POST, unless the form is meant simply as an interface to some complex set of data. The best example of this would be a search form leading to a filtered index. Second, irrespective of the actual method used on the form, the method figures out what the best REST verb is. This later comes into play as a hidden form element that, as we saw very early on while studying the Merb fundamentals, overrides the actual request method used from the perspective of the router. This again is meant to compensate for the lack of availability of all HTTP methods among browsers. Third, the method allows us to use :multipart => true as shorthand for specifying that the form should accept uploads.
From the Library of Shirong Chen
6.6
Form helpers
181
public %w(text password hidden file).each do |kind| self.class_eval control_value(method) }.merge(attrs)) end def unbound_#{kind}_field(attrs) update_unbound_controls(attrs, "#{kind}") self_closing_tag(:input, {:type => "#{kind}"}. merge(attrs)) end RUBY end
Note that the methods for several fields are dynamically generated. This is because there is inherently nothing special about them. The biggest difference between bound and unbound variants is that the bound variants automatically set a name and value based upon the object of the form context before invoking the unbound method. Below we see the private methods used to generate the name and value attributes. private def control_name(method) @obj ? "#{@name}[#{method}]" : method end def control_value(method) value = @obj ? @obj.send(method) : @origin.params[method] if Merb.disabled?(:merb_helper_escaping) value.to_s else Merb::Parse.escape_xml(value.to_s) end end
You may also wonder what the method update_*_controls is for, but before we get to that let’s analyze at least one of the more complicated form element fields:
From the Library of Shirong Chen
182
Chapter 6: Helpers public def bound_check_box(method, attrs = {}) name = control_name(method) update_bound_controls(method, attrs, "checkbox") unbound_check_box({:name => name}.merge(attrs)) end def unbound_check_box(attrs) update_unbound_controls(attrs, "checkbox") if attrs.delete(:boolean) on, off = attrs.delete(:on), attrs.delete(:off) unbound_hidden_field(:name => attrs[:name], :value => off) "checkbox", :value => on}.merge(attrs)) else self_closing_tag(:input, {:type => "checkbox"}. merge(attrs)) end end
The checkbox control is more complicated than those we have seen before, or at least the unbound variant is. Notice how the method bound_check_box doesn’t set a field value. Where is this work being done? private def update_bound_controls(method, attrs, type) case type when "checkbox" update_bound_check_box(method, attrs) when "select" update_bound_select(method, attrs) end end def update_bound_check_box(method, attrs) raise ArgumentError, ":value can't be used" if attrs.has_key?(:value) attrs[:boolean] = attrs.fetch(:boolean, true) val = control_value(method) attrs[:checked] = attrs.key?(:on) ? val == attrs[:on] : considered_true?(val)
From the Library of Shirong Chen
6.6
Form helpers
183
end # ... def considered_true?(value) value && value != "false" && value != "0" && value != 0 end
Inside the cascaded call to update_bound_check_box we find that the checked attribute is set to true or false based upon either the matching of the control value with the on value or otherwise when some value generically considered true has been used. Ignoring the specifics for a moment, the method update_bound_check_box exemplifies the extension of form field behavior. This technique is used not only with many of the bound controls but also with many of the unbound ones. def update_unbound_controls(attrs, type) case type when "checkbox" update_unbound_check_box(attrs) when "radio" update_unbound_radio_button(attrs) when "file" @multipart = true end attrs[:disabled] ? attrs[:disabled] = "disabled" : attrs.delete(:disabled) end def update_unbound_check_box(attrs) boolean = attrs[:boolean] || (attrs[:on] && attrs[:off]) ? true : false case when attrs.key?(:on) ˆ attrs.key?(:off) raise ArgumentError, ":on and :off ..." when (attrs[:boolean] == false) && (attrs.key?(:on)) raise ArgumentError, ":boolean => false ... " when boolean && attrs.key?(:value) raise ArgumentError, ":value cannot be ... " end if attrs[:boolean] = boolean attrs[:on] ||= "1"; attrs[:off] ||= "0" end
From the Library of Shirong Chen
184
Chapter 6: Helpers attrs[:checked] = "checked" if attrs.delete(:checked) end # ...
end
Remembering how the bound checkbox eventually defers to the behavior of the unbound checkbox, we see that the code above is used by both methods. The argument errors are ways of making sure developers don’t misuse the helper, while the final line shows us that despite whatever value it may have had in the past, the checked attribute simplifies to the HTML oddity of either being present and equal to "checked" or not there at all. We’ve left out the numerous other methods used behind the scenes by form fields, but we recommend the pure API for help with the specifics of these helpers. If, however, you do run into a bug, you’ll know right where to track it down. The Base class for form builders can be extended. For instance, the following subclass of Base adds labels to forms and is used by the Merb form helpers by default: class Form < Base def label(contents, attrs = {}) if contents if contents.is_a?(Hash) label_attrs = contents contents = label_attrs.delete(:title) else label_attrs = attrs end tag(:label, contents, label_attrs) else "" end end
The method label is used to build a label element, but we don’t typically call this method ourselves: %w(text password file).each do |kind| self.class_eval attrs[:name]} else label_attrs = {} end label_option = attrs.delete(:label) if label_option.is_a? Hash label(label_attrs.merge(label_option)) else label(label_option, label_attrs) end end
Altogether, the application developer should be aware that form helper methods that have not been handed a value for the :label key within the attributes parameter are not preceded by a label. Two other extensions of the form builder come as modules. Take your time understanding both, as they probably serve as the best structure for extending the form helpers for your own applications. module Errorifier def error_messages_for(obj, error_class, build_li, header, before) obj ||= @obj return "" unless obj.respond_to?(:errors) sequel = !obj.errors.respond_to?(:each) errors = sequel ? obj.errors.full_messages : obj.errors return "" if errors.empty? header_message = header % [errors.size, errors.size == 1 ? "" : "s"] markup = %Q{} +
From the Library of Shirong Chen
186
Chapter 6: Helpers Q{#{header_message}
} errors.each do |err| markup 'delete-btn', :action => url, : method => :post do tag(:input, :type => :hidden, :name => "_method", : value => "DELETE") button_text, : type => :submit)) end end end
Finally, the delete_button method creates a delete form consisting of only a hidden element and a delete button. This helper needs either a specific object to delete or a URL pointing to the path requesting its deletion. The reason for this is simply to avoid misuse of GET requests to alter persisted states on the server.
From the Library of Shirong Chen
192
Chapter 6: Helpers
6.7 Conclusion In this chapter, we explored in depth the code build helpers. That said, both application developers and framework enthusiasts should be able to take away direct insight into how to extend the controllers and views of Merb applications with just about any kind of helper method. We also saw, principally through the form helpers, how a model object can more easily be used within views, tucking away applicable logic almost exclusively into helper code.
From the Library of Shirong Chen
C HAPTER 7
Slices
As we add more features to a Merb application, we may find that some are generic enough to be applicable to other apps. Whereas in the past you may have copied and pasted the relevant controller, views, and models from one application to another, Merb slices allow you to genuinely refactor the code encapsulating them within a gem. This gem can then be included with multiple applications, allowing for application-level overriding of particulars, for instance. In this chapter we’ll go through both the anatomy of slices as well as the source that makes them possible.
7.1 Slice development Slice development practices have been designed to mimic those of standard applications. There are some differences, though, that you need to be aware of. In this section we’ll shed some light on the basics of generating, running, and using a slice.
7.1.1 Generating slices The generation of a slice is similar to the generation of a Merb application. Using merb-gen, we can create the skeleton for our slice: vas@nocturne:˜/src$ merb-gen slice merb-forums-slice Generating with slice generator: [ADDED] README [ADDED] lib/merb-forums-slice.rb [ADDED] Rakefile [ADDED] spec/requests/main_spec.rb [ADDED] spec/spec_helper.rb [ADDED] spec/merb-forums-slice_spec.rb [ADDED] TODO
193 From the Library of Shirong Chen
194
Chapter 7: Slices [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED] [ADDED]
stubs/app/controllers/application.rb stubs/app/controllers/main.rb app/controllers/application.rb app/controllers/main.rb app/helpers/application_helper.rb app/views/main/index.html.erb app/views/layout/merb_forums_slice.html.erb config/router.rb config/init.rb public/javascripts/master.js public/stylesheets/master.css LICENSE lib/merb-forums-slice/merbtasks.rb lib/merb-forums-slice/slicetasks.rb lib/merb-forums-slice/spectasks.rb
You’ll notice that some of these files are particular to slices, most importantly the files in lib and in stub. Let’s open up the most important of them, lib/merb-forumsslice.rb: if defined?(Merb::Plugins) $:.unshift File.dirname(_ _FILE_ _) dependency 'merb-slices', :immediate => true Merb::Plugins.add_rakefiles( "merb-forums-slice/merbtasks", "merb-forums-slice/slicetasks", "merb-forums-slice/spectasks") Merb::Slices::register(_ _FILE_ _) Merb::Slices::config[:merb_forums_slice][:layout] ||= :merb_forums_slice module MerbForumsSlice # Slice metadata self.description = "MerbForumsSlice!" self.version = "0.0.1" self.author = "Engine Yard" def self.loaded end def self.init end
From the Library of Shirong Chen
7.1
Slice development
195
def self.activate end def self.deactivate end def self.setup_router(scope) scope.match('/index(.:format)'). to(:controller => 'main', :action => 'index'). name(:index) scope.match('/'). to(:controller => 'main', :action => 'index'). name(:home) scope.default_routes end end MerbForumsSlice.setup_default_structure! end
The first thing to note is that the code adds the lib directory to the list of locations from which required files can be found. This makes the slice’s local dependencies easy to find even from the host application. The code then proceeds to add various rake task files that may be used for slice maintenance. If your slice needs custom rake tasks, inside one of these files is the best place to put them. The next thing done is the registration of the slice. Note that the register method uses the full fill path to do this. If we peek into the method itself, located in the merb-slices source, we can understand why: module Merb::Slices def register(slice_file, force = true) identifier = File.basename(slice_file, '.rb') underscored = identifier.gsub('-', '_') module_name = underscored.camel_case slice_path = File.expand_path( File.dirname(slice_file) + '/..') if !self.paths.include?(slice_path) || force self.files[module_name] = slice_file self.paths[module_name] = slice_path slice_mod = setup_module(module_name) slice_mod.identifier = identifier slice_mod.identifier_sym = underscored.to_sym slice_mod.root = slice_path slice_mod.file = slice_file
From the Library of Shirong Chen
196
Chapter 7: Slices
slice_mod.registered slice_mod else Object.full_const_get(module_name) end end
The snippet of source above reveals that register prefers the filename so that we can register multiple cascading slices of the same name. The two most important things to take away from this are that slice module names are inferred from the included file’s filename and that a second parameter allows us to turn off the cascading nature of slices by passing in false. Going back to lib/merb-forums-slice.rb, we come across the module MerbForumsSlice and can recognize its importance in being named such. At the start of this module is some metadata that could be used by the master application to identify what slices and slice versions it is using. Below this we find empty hook methods that will allow us to enhance the activation and deactivation of the module. The final and possibly most interesting section of the module is the router block. Though a config/router.rb exists for standardly generated Merb slices, it is intended only for testing isolated slices and not employed by slices that are activated through full apps. Instead, the method setup_router is used to scope slices relative to slice paths. This is critically important for the slice developer to remember: Router configuration happens in the slice file and requires the use of scope. Otherwise, routes are defined exactly the same.
7.1.2 Running slices Having generated a slice, we can fire it up as if it were an isolated application. We can use the command slice from within the slice’s top directory in the same way we would use merb: foysavas@nocturne:˜/src/merb-forums-slice$ slice -C Loading init file from /home/foysavas/src/merb-forumsslice/config/init.rb ˜ Loaded slice 'MerbForumsSlice' ... ˜ Parent pid: 11741 ˜ Activating slice 'MerbForumsSlice' ... merb : worker (port 4000) ˜ Starting Mongrel at port 4000 merb : worker (port 4000) ˜ Successfully bound to port 4000 merb : worker (port 4000) ˜ Interrupt a second time to quit. ˆC irb(main):001:0> MerbForumsSlice.version => "0.0.1"
From the Library of Shirong Chen
7.1
Slice development
197
As you can see, we were even able to use the console trap option -C and then pull up an IRB console for our slice. Additionally, if we visit localhost:4000, we’ll be presented with a simple page showing off the metadata from our slice. You may be interested in knowing how the slice command works and how it is different from the standard merb command. Opening up the executable file itself, we can differentiate the two: #!/usr/bin/env ruby require 'rubygems' require 'merb-core' # Start slice only stuff require 'merb-slices' _ _DIR_ _ = Dir.pwd slice_name = File.basename(_ _DIR_ _) Merb::Config.use { |c| c[:framework] = { :public => [Merb.root / "public", nil] } c[:session_store] = 'none' c[:exception_details] = true } if File.exists?(slice_file = File.join( _ _DIR_ _, 'lib', "#{slice_name}.rb")) Merb::BootLoader.before_app_loads do $SLICE_MODULE = Merb::Slices. filename2module(slice_file) require slice_file end Merb::BootLoader.after_app_loads do Merb::Router.prepare do slice($SLICE_MODULE) slice_id = slice_name.gsub('-', '_').to_sym slice_routes = Merb::Slices. named_routes[slice_id] || {} route = slice_routes[:home] || slice_routes[:index]
From the Library of Shirong Chen
198
Chapter 7: Slices if route params = route.params.inject({}) do |hsh,(k,v)| hsh[k] = v.gsub("\"", '') if k == :controller || k == :action
hsh end match('/').to(params) else match('/').to(:controller => 'merb_slices', :action => 'index') end end end else puts "No slice found (expected: #{slice_name})" exit end class MerbSlices < Merb::Controller def index html = "#{slice.name}"+ "#{slice.description}
" html '/community' end
Here’s the code for the slice extension method: def slice(slice_module, options = {}, &block) options[:path] ||= "" add_slice(slice_module, options.merge(:reset_controller_prefix => true), &block) end
The method merges in the option to reset the controller prefix. This is necessary because without it a controller prefix is used on the routes. Aside from that, slice basically
From the Library of Shirong Chen
202
Chapter 7: Slices
delegates to add_slice, another router extension method that we advise application developers to stay away from unless they intend to use a controller prefix. For the sake of clarity on other options, here’s the source to add_slice: def add_slice(slice_module, options = {}, &block) if Merb::Slices.exists?(slice_module) options = { :path => options } if options.is_a?(String) slice_module = Object.full_const_get( slice_module.to_s.camel_case) if slice_module.class.in?(String, Symbol) namespace = options[:namespace] || slice_module.identifier_sym options[:path] ||= options[:path_prefix] || slice_module[:path_prefix] || options[:namespace] || slice_module.identifier options[:prepend_routes] = block if block_given? slice_module[:path_prefix] = options[:path] Merb.logger.verbose!("Mounting slice ... ") @options[:controller_prefix] = nil if options.delete(:reset_controller_prefix) self.namespace(namespace, options.except( :default_routes, :prepend_routes, :append_routes, :path_prefix)) do |ns| Merb::Slices.named_routes[ slice_module.identifier_sym] = ns.capture do options[:prepend_routes].call(ns) if options[:prepend_routes].respond_to?(:call) slice_module.setup_router(ns). options[:append_routes].call(ns) if options[:append_routes].respond_to?(:call) end end else
From the Library of Shirong Chen
7.3
Conclusion
203
Merb.logger.info!("Skipped adding slice ... ") end self end
From the perspective of a student of the frameworks, the most interesting code above is in the lines where the route namespace is created. Particularly be aware of the use of the behavior method capture, which simply collects a hash of routes as they are added within its block without affecting the routes themselves in any way. This method, which you may have written off, actually allows Merb to store a partitioned list of routes related to particular slices, a necessity for the dynamic deactivation of slices.
7.3 Conclusion Merb slices are one of the most attractive features from an application developer’s perspective because they increase the ease and reusability of code. The second version of Merb was intended to make great strides toward dynamic activation of Merb slices and build an arsenal of slices for developers to share. However, given the merge, such work has been put on hold until a similar system is settled upon within Rails 3.
From the Library of Shirong Chen
This page intentionally left blank
From the Library of Shirong Chen
C HAPTER 8
Sessions
Sessions allow an application to customize the responses sent out to different users. They do this by enabling us to store data about each session and use that data as the foundation for statefulness between requests. When used in conjunction with user authentication, session data also means that we can trust that a request comes from a particular user without having to reauthenticate on subsequent requests. Strictly speaking, HTTP is a stateless protocol, and every request that comes to an HTTP server is treated like any other. However, at an application level we can transcend the limitations of statelessness using sessions. If we had to construct sessions from the ground up, we would have to design a way to distribute HTTP cookies with session IDs and then session data stored either client- or server-side. However, Merb’s builtin session support eliminates the need to do any of this work, typically streamlining it down to the selection of configuration options and interaction with a hash named session. Merb’s emphasis on hackabiliy is not lost, however, through the streamlining of session maintenance. In this chapter we’ll explore the construction of session containers and stores and find that building them poses little difficulty to the developer with such a need. One warning, though: Sessions also incidentally bring up security concerns, but we’ll point these out when they arrive and explain how to deal with them.
8.1 How sessions work Assuming that sessions are enabled on a Merb application, a session ID is established for each capable browser that sends a request. This session ID is stored client-side as an encrypted value inside an HTTP cookie. All subsequent requests made by the user’s browser will include this cookie as a header, and Merb will decrypt the session ID 205 From the Library of Shirong Chen
206
Chapter 8: Sessions
from it. In this way we can establish and maintain the identity of the individual users of our application. However, data related to each session must be stored separately, and Merb provides us with multiple storage options. These include saving data in memory, in a database, cached with memcache, or even within the HTTP cookie itself. Each of these options is appropriate under different conditions. We’ll go over when and how to use them later on.
8.2 Configuration Not all applications need sessions to the same degree and some do not need them at all. At the same time, different environments make different demands on how session data must be stored and accessed. For these reasons, our sessions must be configured. Configuration is typically done in either config/init.rb or one of the environment files in config/environments. Remember that any configuration settings made in an environment file override what is in the init script. Consequently, the configuration in the init script tends to be the development configuration and acts only as a template for the other environments. There are five basic values for configuration that can be set, but depending on the storage mechanism there may be additional settings to configure: • session_store—the type of session to use: cookie, memory, memcache, container,
datamapper, etc. • session_id_key—defaults to _session_id but can be useful when you need to
differentiate sessions for the same domain • session_expiry—defaults to two weeks but its value can be set as an integer in
seconds • session_secret_key—a string of at least 16 chars used for the encryption of the
cookie session store • default_cookie_domain—the domain the cookies are for
Typically, some of these values are set from within config/init.rb as follows: Merb::Config.use do |c| c[:session_store] = 'cookie' c[:session_secret_key] = 'theywontguessthis' c[:session_id_key] = '_app_session_id' end
From the Library of Shirong Chen
8.3
Storing sessions
207
8.3 Storing sessions The mechanics behind sessions are for the most part not necessary for an application developer to know. However, in the interest of understanding the tools we use, we’ll cover the topic in some detail. Before jumping in, you should know that there are two central classes to sessions storage: SessionContainer and SessionStoreContainer.
8.3.1 Session containers The fundamental structure storing individual session data in Merb is a session container. The class SessionContainer inherits from Mash, making its keys accessible by either symbol or equivalent string. The reason the Mash class has been used here again is so that request parameter data may easily be stored within session containers without complication. module Merb class SessionContainer < Mash class_inheritable_accessor :session_store_type cattr_accessor :subclasses self.subclasses = [] # :api: private attr_reader :session_id # :api: private attr_accessor :needs_new_cookie class 'app') end
Pay special attention to the need to require the memcache gem as well as to have the code placed within an after_app_loads block.
8.3.3.4 DataMapper sessions DataMapper sessions are an alternative way to accomplish production-suitable sessions without the need for a memcache server. This may suit the needs of smaller production environments where putting up memcache is not a possibility. However, we still strongly recommend not using database sessions if for no other reason than session data is retrieved frequently, and memcache is designed to handle that better. Nonetheless, as an exploration of the versatility of Merb sessions, let’s take a look at how it’s done: module Merb class DataMapperSession < SessionStoreContainer self.session_store_type = :datamapper self.store = DataMapperSessionStore end
There’s nothing special so far, but by now you should be capable of making your own store and connecting it to the container.
From the Library of Shirong Chen
218
Chapter 8: Sessions
class DataMapperSessionStore include ::DataMapper::Resource table_name = Merb::Plugins. conf[:merb_datamapper][:session_storage_name] || 'sessions' storage_names[default_repository_name] = table_name def self.default_repository_name Merb::Plugin. conf[:merb_datamapper][:session_repository_name] || :default end
In this way, a DataMapper resource has effectively been created, but special attention has been given to the name of the table and the repository to use. These two variables can be altered within our configuration files. property :session_id, String, :size => 32, :nullable => false, :key => true property :data, Object, :default => {}, :lazy => false property :created_at, DateTime, :default => Proc.new { |r, p| DateTime.now }
The three properties above form the basis of DataMapper session storage. Note that created_at has used its own Proc, meaning that no requirements on dm-timestamps
are made. Additionally, we see a good use of the database-admin-frightening property type Object. It’s important that it’s not lazy-loaded since its primitive type is Text, and we will want to access the session data whenever we pull up the record. def self.retrieve_session(session_id) if session = get(session_id) session.data end end def self.store_session(session_id, data) if session = get(session_id) session.update_attributes(:data => data) else create(:session_id => session_id, :data => data) end end def self.delete_session(session_id)
From the Library of Shirong Chen
8.4
Request access
219
all(:session_id => session_id).destroy! end end end
Finally, the three methods we’ve come to expect appear again, this time using the DataMapper retrieval methods to do their work.
8.4 Request access Within Merb, sessions are glued onto their associated requests. To accomplish this, the RequestMixin adds several methods to the Request class. Only the first is of importance to the typical application developer, so we’ll take a look at it, letting the reader dig deeper if desired. module Merb::Session::SessionMixin::RequestMixin # :api: public def session(session_store = nil) session_store ||= default_session_store if class_name = self.class.registered_session_types[session_store] session_stores[session_store] ||= Object.full_const_get(class_name).setup(self) elsif fallback = self.class.registered_session_types.keys.first Merb.logger.warn "Session store not found." Merb.logger.warn "Falling back." session(fallback) else msg = "No session store set." Merb.logger.error!(msg) raise NoSessionContainer, msg end end
Note that if we are using multiple session stores, we can specify the desired one to check inside. If it’s not found, Merb falls back to the first session store defined. A practical use of the method from the application developer’s perspective would be the use of session data from inside router deferral blocks.
From the Library of Shirong Chen
220
Chapter 8: Sessions
8.5 Controller access The controller also needs access to the session. This is likewise accomplished through a controller session mixin: module Merb::Session::SessionMixin # :api: public def session(session_store = nil) request.session(session_store) end module RequestMixin # ... end end
Here we see that the session method simply delegates to the method we saw defined within the request mixin. It is somewhat intriguing that the RequestMixin we saw a moment ago is actually embedded within the controller mixin. This kind of organization is typical among Merb request enhancements and minimizes redundancy. Anyway, as application developers, we have full access to the session data via this method, and treating it as a hash can set its values.
8.6 Conclusion Merb handles sessions in such a way that application developers do not have to be aware of the storage mechanisms being used. Nonetheless, the limitations and security implications behind some session storage, mainly cookie storage, demand the developer’s awareness when they are used. Our exploration of the internals of both session containers and stores should make it easy to extend Merb sessions as needed for custom or composite storage mechanisms when the nearly standard production use of memcache will not do.
From the Library of Shirong Chen
C HAPTER 9
Authentication
Nearly all web applications need to authenticate their users, but how they authenticate them varies. The Merb stack, however, includes a highly modular authentication plugin that gives us a miniature stack of minimally constraining tools that together offer us out-of-the-box authentication within our applications. Created by Daniel Neighman, the Merb auth plugin went through several intense iterations. Before long, however, its design came to exemplify the principle of Merb itself and without hesitation was included within the stack, ultimately because its modularity countered the contentions of custom authentication so strongly. This also makes it one of the absolute best plugins from which to learn, so we’re going to explore it nearly in its entirety. We recommend that you take your time and keep several goals in mind while we do so. First, as a Merb application developer, remember the methods that you’ll need to use. These will also be listed among other documentation, however, so another goal should be obtaining a deeper understanding of how they work. From the perspective of a Merb plugin developer, you should also pick up on the structural considerations apparent throughout the plugin. Various techniques will indubitably come in handy during the construction of your own plugins. Last, as a student of Merb’s design itself, look to this chapter to impart a deeper understanding of the envisioned extensibility of Merb.
9.1 Auth core The core part of merb-auth is divided into several sections. We can find the code breakdown spelled out in the file lib/merb-auth-core.rb: require 'merb-auth-core/authentication' require 'merb-auth-core/strategy'
221 From the Library of Shirong Chen
222 require require require require require require require require
Chapter 9: Authentication 'merb-auth-core/session_mixin' 'merb-auth-core/errors' 'merb-auth-core/responses' 'merb-auth-core/authenticated_helper' 'merb-auth-core/customizations' 'merb-auth-core/bootloader' 'merb-auth-core/router_helper' 'merb-auth-core/callbacks'
Merb::BootLoader.before_app_loads do Merb::Controller.send(:include, Merb::AuthenticatedHelper) end Merb::Plugins.add_rakefiles "merb-auth-core/merbtasks" Merb.push_path(:lib_authentication, Merb.root_path("merb" / "merb-auth"), "*.rb" )
We’ll go through the significance and use of each of the files within this section. Note the use of before_app_loads for the inclusion of the authenticated helper. These lines exemplify the common need for methods defined in a plugin to be loaded before application code is loaded, lest the application code blow up. This is because the authenticated helper adds a method, ensure_authenticated, which the application developer may use to ensure the authentication of users. We can also spot the use above of Plugins.add_rakefiles to include various rakefile tasks. This appends rakefiles to the array Merb.rakefiles, which are all iteratively required within the standardly generated Merb application rakefile. Finally, the use of Merb.push_path is a method that adds files for inclusion within our Merb application. For the authentication plugin, and many other plugins, this is critical because it allows for particular code to be contained within the application itself, overriden by the developer as necessary. In the case of the authentication plugin, there are two such files of importance: setup.rb and strategies.rb. Let’s look at the first of these two: Merb::Authentication.user_class = User require 'merb-auth-more/mixins/salted_user' Merb::Authentication.user_class.class_eval do include Merb::Authentication::Mixins::SaltedUser end class Merb::Authentication def fetch_user(session_user_id) Merb::Authentication.user_class.get(session_user_id) end
From the Library of Shirong Chen
9.1
Auth core
223
def store_user(user) user.nil? ? user : user.id end end
The first thing this does, and that you may need to change, is select which model class will be used for authentication. The standard class is User, as we can see above, but if your application demands that it be something else, such as Person, changing the value within setup.rb allows you to use the authentication plugin without issue. The next few lines add in the salted user mixin. We’ll see the code for this later on, but for now know that this adds properties and methods to the user model for authentication. The last section of the code opens up the Authentication class, adding in two methods. The first wraps the manner of retreiving a user by ID, and the second defines how a user should be stored within a session. Don’t let the tertiary operator confuse you, however. All that’s being said is that a user’s ID alone should be stored within the session store. Now let’s take a look at the second file, strategies.rb: Merb::Slices::config[:"merb-auth-slice-password"][: no_default_strategies] = true Merb::Authentication.activate!(:default_password_form) Merb::Authentication.activate!(:default_basic_auth)
We see that the default strategies are cleared out and then two of them are reactivated. We’ll find out more about this soon, but from this alone we get a feel for how the application developer can interact with cascading authentication strategies.
9.1.1 Authentication Let’s take a look at parts of the first file required by the authentication core, which defines Merb::Authentication: module Merb class Authentication module Strategies; end include Extlib::Hook attr_accessor :session attr_writer :error_message class DuplicateStrategy < Exception; end class MissingStrategy < Exception; end class NotImplemented < Exception; end
From the Library of Shirong Chen
224
Chapter 9: Authentication def initialize(session) @session = session end def authenticated? !!session[:user] end def user return nil if !session[:user] @user ||= fetch_user(session[:user]) end def user=(user) session[:user] = nil && return if user.nil? session[:user] = store_user(user) @user = session[:user] ? user : session[:user] end def abandon! @user = nil session.clear end
Note that the contained module Strategies is stubbed and will be used later on to contain various means of authentication. The inclusion of Extlib::Hook is also noteworthy; it allows the authentication plugin to make particular methods hookable for the application developer. Moving on to the methods, we can see that an authentication is tightly coupled with a session store. It also specifically stores user data within the session store under the key :user. This means that application developers should stay away from using this key for other purposes. The double bang within the authenticated? method assures that either true or false is returned even if session[:user is nil. The method user returns nil whenever the session user key isn’t holding a user. Otherwise, it fetches the user or, if that has already been done, returns a memoized user. The corresponding writer method delegates to the method store_user, which we previously saw in setup.rb. Going further into the file, we can see stubs for a number of methods, all labeled API overwritable: # @api overwritable cattr_accessor :user_class # @api overwritable def error_message @error_message || "Could not log in" end
From the Library of Shirong Chen
9.1
Auth core
225
# @api overwritable def store_user(user) raise NotImplemented end # @api overwritable def fetch_user(session_contents = session[:user]) raise NotImplemented end
Some of these are the same methods we saw in setup.rb. The overwritable therefore specifies that they can be defined within that file by the application developer. The crown jewel of the Authentication class, however, is the method authenticate!. Here it is in full: def authenticate!(request, params, *rest) opts = rest.last.kind_of?(Hash) ? rest.pop : {} rest = rest.flatten strategies = if rest.empty? if request.session[:authentication_strategies] request.session[:authentication_strategies] else Merb::Authentication.default_strategy_order end else request.session[:authentication_strategies] ||= [] request.session[:authentication_strategies] "exceptions", :action => "unauthenticated").name(:login) scope.match("/login", :method => :put ). to(:controller => "sessions", :action => "update").name(:perform_login) scope.match("/logout").
From the Library of Shirong Chen
9.3
Auth password slices
247
to(:controller => "sessions", :action => "destroy").name(:logout) end end # ... end
We see three matches, one of them pointing to the Exceptions controller for login and the other two involving the Sessions controller. It may seem odd at first to use an Exceptions controller for anything the user sees on a regular basis, but once you realize that you can throw Unauthenticated exceptions to get the user to a login page, the decision to do so makes perfect sense. Finally, remember that scope is used to limit the reach of our routes within the main application. Thus, within our application router we’ll probably spot code like that below to get the routes matching at the root level. slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "")
9.3.2 Controller The password slice makes modifications to two controllers, Sessions and Exceptions. Let’s take a look at the Sessions controller first since its construction is the most direct: class MerbAuthSlicePassword::Sessions < MerbAuthSlicePassword::Application before :_abandon_session, :only => [:update, :destroy] before :ensure_authenticated, :only => [:update] after :redirect_after_login, :only => :update, :if => lambda{ !(300..399).include?(status) } after :redirect_after_logout, :only => :destroy def update "Add an after filter to do stuff after login" end def destroy "Add an after filter to do stuff after logout" end
From the Library of Shirong Chen
248
Chapter 9: Authentication
What we notice first is the use of underscored filters. The underscores are there to prevent conflict with your own. More important, though, we can conclude that there are two actions in this controller, update and destroy. In either case, a session will be abandoned, but in the case of update, ensure_authenticated will come into play by authenticating our session, as was previously seen. Additionally, the two after filters do not have underscores and are meant to be overwritten if desired. private # @overwritable def redirect_after_login message[:notice] = "Authenticated Successfully" redirect_back_or "/", :message => message, :ignore => [slice_url(:login), slice_url(:logout)] end # @overwritable def redirect_after_logout message[:notice] = "Logged Out" redirect "/", :message => message end # @private def _abandon_session session.abandon! end end
If not, they behave as above, using the method redirect_back_or to direct the user appropriately after login or logout. Finally, we see that _abandon_session simply relays to a method we saw in Chapter 8, abandon!. Moving on to the code for the Exceptions controller, we’ll warn you that it has been structured as a mixin: module MerbAuthSlicePassword::ExceptionsMixin def unauthenticated provides :xml, :js, :json, :yaml case content_type when :html render else basic_authentication.request! "" end end # unauthenticated end
From the Library of Shirong Chen
9.3
Auth password slices
249
Simply enough, though, the method authenticated looks like an action and either renders an HTML template or attempts basic authentication otherwise using the Merb core method basic_authentication.request!. Encountering the use of the authentication boot loader method customize_default, we find the reason for the mixin above: Merb::Authentication.customize_default do Exceptions.class_eval do include Merb::Slices::Support # Required to provide slice_url # # This stuff allows us to provide a default view the_view_path = File.expand_path(File.dirname( _ _FILE_ _) / ".." / "views") self._template_roots ||= [] self._template_roots
Of course, if you make this form for yourself, there’s no reason not to use the Merb helper, which out of courtesy has not been used above. Also, you probably want to refer to login and password by indirect references. The most interesting line is the one with the method error_messages_for. Remember the errors included within Authentication? It’s nice to finally put them to use as if they were model validation errors.
From the Library of Shirong Chen
9.4
Conclusion
251
9.4 Conclusion Nearly all web applications need to authenticate their users. With merb-auth, Adam French, Daniel Neighman, and other contributors have isolated the core mechanism of authentication, providing a fully modular approach to its additional features. Our tour of the code behind merb-auth, probably the pinnacle of beautiful Merb plugins, has given us examples of both low-level plugins as well as a high-level slice. We recommend that all students of the Merb framework grok the code as a supplement to this chapter.
From the Library of Shirong Chen
This page intentionally left blank
From the Library of Shirong Chen
C HAPTER 10
Mailers
Many applications need to send email. Merb Mailers allow you to do this easily with the same controller and view conventions the rest of your application follows. The Mailer class is thus a subclass of AbstractController, and the relevant templates are standardly placed in app/mailers/views. In this chapter we’ll configure, generate, describe, and test mailers used in Merb applications.
10.1 Configuration If you don’t configure Merb otherwise, it tries to use SMTP locally when asked to deliver mail. In most cases these exact defaults do not suffice, but alternative configurations are as simple as a few extra lines in either config/init.rb or one of the environment config files in a before_app_loads block. Below we describe the various configuration options available, separated by delivery method.
10.1.1 SMTP Though SMTP is the default for the Merb Mailer, chances are you’ll need to configure it more explicitly. This may involve specifying the host, port, user, password, authentication method, and HELO domain. Below we do this by setting various hash values for Merb::Mailer.config. Merb::Mailer.config = { :host => 'smtp.gmail.com', :port => 587, :user => '[email protected]', :password => 'password', :auth => 'plain' }
253 From the Library of Shirong Chen
254
Chapter 10: Mailers
Note that if you are using the Gmail SMTP servers or some other TLS-required SMTP service, you will need Anatol Pomozov’s gem smtp_tls in order to enhance some of the net_smtp methods.
10.1.2 Sendmail In order to use sendmail, you must specify the mailer delivery method in a configuration file. This overrides the default of :net_smtp. You may also need to alter the default sendmail path from /usr/sbin/sendmail. Here we do both: Merb::Mailer.delivery_method = :sendmail Merb::Mailer.config = {:sendmail_path => '/usr/local/sbin /sendmail'}
10.1.3 Test send A third option, perfect for both test and possibly development environments, is the test_send delivery method. This method does not actually send email but instead pushes the message into the array Merb::Mailer.deliveries: Merb::Mailer.delivery_method = :test_send
10.1.4 Custom delivery methods You can also create custom delivery methods for your applications. For instance, if the SMTP gateway you’re using severely penalizes attempts to send messages above the daily quota, you may want to construct a custom delivery method that first checks to make sure you haven’t surpassed your limit. By adding a couple of methods within Merb::Mailer, we can do just this: class Merb::Mailer def net_smtp_with_quota if increase_quota_use net_smtp end end def increase_quota_use # ... end end Merb::Mailer.delivery_method = :net_smtp_with_quota
From the Library of Shirong Chen
10.2
Using mailers directly
255
Note that in the example above, we haven’t included the actual methods for checking and increasing quota usage. This is because implementation will indubitably differ with the circumstances. One warning, though: Make sure the implementation is thread-safe. Otherwise, despite all your efforts, you may go beyond your quota anyway. Should this really be part of configuration? While Merb mail controllers, as we’ll soon learn, are sophisticated enough to employ filters, some cases are better suited to exist as configuration code. There are a couple of reasons why. First, by choosing to implement quota checking or some similar functionality as part of a delivery method as opposed to in a filter, we are assured that it applies to all messages sent out by the application, including those that may not use a mail controller. Second, by locating the code in configuration files, the existence of the mail quota does not tie itself directly to our application code. Thus, should we need to work in multiple production environments, we won’t have to deal with any leftover cruft.
10.2 Using mailers directly Though it’s not advisable, you can use mailers directly to send out messages. In most cases we recommend the use of the Merb Mail Controller, which is covered in the next section. However, because you may come across a scenario where you need to send out mail from inside some other noncontroller class, let’s cover its use directly. Internally, Merb::Mailer interfaces to David Powers’s MailFactory. Passing in a hash during initialization, you construct the email just as if you were using MailFactory itself and set the attributes of a message. Below we create and send a new message by first initializing a mailer object and then calling the Mailer#deliver! method. m = Merb::Mailer.new( :to => '[email protected]', :from => '[email protected]', :subject => 'Pro Ruby Conf', :text => 'Great meeting you!' ) m.deliver!
Some email clients are capable of also rendering HTML email. The Merb Mailer allows you to send HTML emails by simply setting the :html key instead of the :text key. However, it is standard practice to also include a text-only version whenever sending out HTML messages. Such dual-formatted messages are called multipart messages.
From the Library of Shirong Chen
256
Chapter 10: Mailers
Constructing a multipart message with the Merb Mailer is as simple as setting both the :text and :html keys at initialization: m = Merb::Mailer.new( :to => '[email protected]', :from => '[email protected]', :subject => 'Pro Ruby Conf', :text => 'Great meeting you!', :html => '<strong>Great meeting you!' )
Let’s take a look at the code behind Merb::Mailer to understand how it interfaces with MailFactory: module Merb class Mailer class_inheritable_accessor :config, :delivery_method, :deliveries attr_accessor :mail self.deliveries = [] def sendmail sendmail = IO.popen( "#{config[:sendmail_path]} #{@mail.to}", 'w+') sendmail.puts @mail.to_s sendmail.close end def net_smtp Net::SMTP.start( config[:host], config[:port].to_i, config[:domain], config[:user], config[:pass], config[:auth]) { |s| s.send_message(@mail.to_s, @mail.from.first, @mail.to.to_s.split(/[,;]/)) } end def test_send deliveries invite[:to] }) redirect url(:new_invite) end end
We recommend that you send mail out of sync with the construction of responses since SMTP gateways can hold you up at times. The easiest way to do this is with a run_later block. Here we use run_later in a slightly more complex controller action: class Invites < Application def create(invite) @invite = Invite.new(invite) if @invite.save run_later do send_mail(InviteMailer, :notify, { :from => MAILER_EMAIL, :to => @invite.email, :subject => "You're invited!" }) end redirect url(:new_invite) end end end
10.3.2 Parameters Mail controllers by default have access to invoking a controller’s parameters. To access them with the mail controller, we simply use the params mash as we would in a standard mail controller. We can, however, override the value of params by using a fourth parameter on the action Controller#send_mail. Below we pass in a custom hash of parameters to be used from within the mail controller. class UserMailer def notify_on_signup @mail.subject = "You signed up!" @mail.text = "Welcome #{params[:user][:full_name]}!"
From the Library of Shirong Chen
260
Chapter 10: Mailers
@mail.from = SUPPORT_EMAIL end end class Users < Application def create(user) @user = User.new(user) if @user.save run_later do send_mail( UserMailer, :notify_on_signup, {:to => @user.email}, {:user => @user} ) end end redirect url(:home) end end
10.3.3 Attaching files Files can be attached to messages using the method MailController#attach. This method takes either one or two parameters. When you use it to attach a single file, the first parameter should be the file. Optionally, the second parameter may be an alternate filename used when the message is actually sent. This may be useful if you are personalizing documents. Below we attach two documents, one at a time. Both will be included in the final email. attach(File.open '/data/documents/terms.txt') attach(personalized_invoice, "#{account_id}_invoice.txt")
Instead of passing in an array to the attach method, we can attach multiple documents at once: attach([first_file, second_file])
Finally, we can also manually specify the MIME type and final filename using arrays of arrays: attach([[ first_file, "#{account_id}_invoice.html",
From the Library of Shirong Chen
10.5 Generation
261
"text/html" ]]
10.3.4 Templates Of course the principal attraction of using mail controllers is the rendering of mail message views. These are not stored in the same location as standard views but instead in /app/mailers/views. Each mail controller should have its own subdirectory within this directory just like a standard controller. This subdirectory should be a snake-cased version of the mail controller’s own name in full, such as invite_mailer/ for the InviteMailer. Mailer templates are rendered just the same as regular templates, so you should encounter no other complications.
10.4 Testing Testing mailers can seem tricky since they’re not part of the standard request-response cycle. However, using a few extra methods alongside the :test_method delivery method makes testing mailer behavior no problem at all. describe InviteMailer, "#notify" do before :each do Merb::Mailer.deliveries.clear end it "should include invite code" do InviteMailer.dispatch_and_deliver(:notify, { :invite_code => "QWERTY88" }) Merb::Mailer.deliveries.last.text.should =˜ "Your code is: QWERTY88" end end
In the code above we use the array Merb::Mailer.deliveries, the array to which test delivery pushes, to test the results of the mailer against our expectations.
10.5 Generation Finally, application developers should be aware that they can quickly generate the numerous files that mailers typically entail using merb-gen. Below we create the invite mailer.
From the Library of Shirong Chen
262 $ merb-gen Generating [ADDED] [ADDED] [ADDED]
Chapter 10: Mailers mailer invite with mailer generator: app/mailers/invite_mailer.rb app/mailers/views/invite_mailer/notify_on_event.text.erb spec/mailers/invite_mailer_spec.rb
10.6 Conclusion Merb Mailers are an excellent example of the intelligence of the Merb architecture. Subclassing off of the abstract controller base class, the Merb mail controller inherits standard controller functionality, allowing the application developer to deal with the sending of email through the same best practices used for the rest of the app.
From the Library of Shirong Chen
C HAPTER 11
Parts
Widgets are everywhere on the web. These component-like regions on web pages deliver high levels of functionality. Though many of them are Ajax-dependent, having the same functionality as standard responses really expands the possibilities of our applications. To this effect, the gem merb-parts subclasses AbstractController, providing a new type of controller whose responses can be emebedded like partials within the actual response to a request.
11.1 Parts controllers Parts controllers are used in nearly the same way as the standard Merb controller. Here’s an example of a part placed in app/parts/: class GraphPart < Merb::PartController def index @log = params[:log] @width = params[:width] || 690 @height = params[:height] || 360 if params[:date].nil? @date = Date.parse(params[:date]) else @date = Date.today end render end end
The most important thing to note is how the controller inherits from Merb::PartController. Otherwise, nearly all the methods you might use in a
263 From the Library of Shirong Chen
264
Chapter 11: Parts
controller, including render, are available. Let’s take a look at the source code behind parts controllers in general: require File.join(File.dirname(_ _FILE_ _), "mixins", "web_controller") module Merb class PartController < AbstractController cattr_accessor :_subclasses self._subclasses = Set.new def self.subclasses_list() _subclasses end def self.inherited(klass) _subclasses 345, :height => 180 %>
Let’s take a look at the mixin that makes this possible by adding in the method part: module Merb module PartsMixin def part(opts = {}) klasses, opts = opts.partition do |k,v| k.respond_to?(:ancestors) && k.ancestors.include?(Merb::PartController) end
From the Library of Shirong Chen
11.4 Conclusion
267
opts = opts.empty? ? {} : Hash[*(opts.first)] res = klasses.inject([]) do |memo,(klass,action)| memo 1}, :expire_in => 3600 )
The first line is the easier of the two, simply setting data to be associated with a value. The second, however, has two extra method parameters, the first being a hash of additionally identifying parameters and the second being a condition that applies only to memcache stores. In order to contrast the two methods and know how they work, let’s take a look at the two implementations of write, first for FileStore and second for MemcachedStore: # for FileStore def write(key, data = nil, parameters = {}, conditions = {}) if writable?(key, parameters, conditions) if File.file?(path = pathify(key, parameters)) write_file(path, data) else create_path(path) && write_file(path, data) end end end # for MemcachedStore def write(key, data = nil, parameters = {}, conditions = {})
From the Library of Shirong Chen
272
Chapter 12: Caching
if writable?(key, parameters, conditions) begin @memcached.set(normalize(key, parameters), data, expire_time(conditions)) true rescue nil end end end
As previously mentioned, both make use of the writable? method, but the file store is more complicated. It creates a path for the file and then writes to it.
12.2.2 Reading Reading, like writing, is handled by two methods, exists? and read. You may end up finding some use for exists? as an application developer, but for the most part read is sufficient since the first thing it does is seek the approval of exists?. Merb::Cache[:default].read("key1") # => "data1" Merb::Cache[:memcached].read("key2", {:id => 1}) # => "data2"
Above we have pulled the data that we previously persisted in the two data stores. # for FileStore def read(key, parameters = {}) if exists?(key, parameters) read_file(pathify(key, parameters)) end end # for MemcachedStore def read(key, parameters = {}) begin @memcached.get(normalize(key, parameters)) rescue Memcached::NotFound, Memcached::Stored nil end end
Note that the read method of FileStore uses exists?, as opposed to DataStore#read, which rescues not-found errors, ignoring them and returning nil.
From the Library of Shirong Chen
12.2
Caching basics
273
12.2.3 Fetching The fetching of records is essentially shorthand for the retrieval or otherwise writing of a cached value. Below we use a block to return the potential value of a cache element that may not exist. Merb::Cache.fetch("key0") do render end
The fetch methods for both of the fundamental stores is the same: def fetch(key, parameters = {}, conditions = {}, &blk) read(key, parameters) || ( writable?(key, parameters, conditions) && write(key, value = blk.call, parameters, conditions) && value ) end
In its highly condensed statement, fetch reads from or otherwise attempts to write to a cached element.
12.2.4 Deleting You can delete cached data using the method delete and passing in the cached element key along with any identifying parameters needed. Observe that the code behind the delete method differs between the two fundamentals, but no more than we’ve seen before: # for FileStore def delete(key, parameters = {}) if File.file?(path = pathify(key, parameters)) FileUtils.rm(path) end end # for MemcachedStore def delete(key, parameters = {}) begin @memcached.delete(normalize(key, parameters)) rescue Memcached::NotFound nil end end
From the Library of Shirong Chen
274
Chapter 12: Caching
12.3 Caching helpers Various helpers exist to ease the use of caching away from the primitive methods we have so far seen. We’ll take a glimpse at all of these in this section.
12.3.1 Action caching In order to better facilitate the page and action stores, a number of methods are mixed into controllers at both an instance and a class level. The simpliest of these is cache!, which caches all actions. Alternatively, the method cache accepts a list of actions to cache. More precisely, we can specify that a particular action should be cached only under certain conditions with cache_action: class Papers < Application cache! # ... end class Users < Application cache :new, :index cache_action :show, :if => important_user? # ... end
All of these work by adding in before filters: def cache!(conditions = {}) before(:_cache_before, conditions.only(:if, :unless). merge(:with => conditions)) after(:_cache_after, conditions.only(:if, :unless). merge(:with => conditions)) end def cache(*actions) if actions.last.is_a? Hash cache_action(*actions) else actions.each {|a| cache_action(*a)} end end def cache_action(action, conditions = {}) before("_cache_#{action}_before",
From the Library of Shirong Chen
12.3
Caching helpers
275
conditions.only(:if, :unless).merge( :with => [conditions], :only => action)) after("_cache_#{action}_after", conditions.only(:if, :unless).merge( :with => [conditions], :only => action)) alias_method "_cache_#{action}_before", :_cache_before alias_method "_cache_#{action}_after", :_cache_after end
Do note that the position of the cache class methods can have an effect on the ultimate request, since they may be part of a complex filter chain. Authentication filters, for instance, should certainly appear above cache method lines.
12.3.2 Eager caching A variant of action caching that is better suited for pages where the most current information is mildly less important than the speed of the response is eager caching. Making use of after filters instead of before filters, eager caching updates stale versions of the cache based upon the evoking of a triggering action. It does so in a run_later block after the triggering action is evoked, making eager caching perfect for actions with expensively constructed responses. However, that’s the only problem eager caching solves, since it also effectively marks as stale all old caches. This versatility can be seen in the example below, where we employ the method eager_cache. class Users < Application eager_cache :create, :index eager_cache :update, :show do |params| build_request(build_url(:user, :id => params[:id]), :id => params[:id]) end end
The two methods build_url and build_request are helper methods used by the eager_cache method to construct URLs and internally dispatch requests, respectively. In the first case above we don’t pass in a block and let eager_cache do its own magic. The second is more explicit. Here’s the code that sets up the after filters: def eager_cache(trigger_action, target = trigger_action, conditions = {}, &blk) target, conditions = trigger_action, target if target.is_a? Hash
From the Library of Shirong Chen
276
Chapter 12: Caching
if target.is_a? Array target_controller, target_action = *target else target_controller, target_action = self, target end after("_eager_cache_#{trigger_action}_to_"+ "#{target_controller.name.snake_case}"+ "_ _#{target_action}_after", conditions.only(:if, :unless).merge( :with => [target_controller, target_action, conditions, blk], :only => trigger_action) ) alias_method "_eager_cache_#{trigger_action}_to_"+ "#{target_controller.name.snake_case}_ _"+ "#{target_action}_after", :_eager_cache_after end
But far more interestingly, here’s the code that dispatches the eager requests to be cached. Pay special attention to the variety of forms in which requests can be created. def eager_dispatch(action, params = {}, env = {}, blk = nil) kontroller = if blk.nil? new(Merb::Request.new(env)) else result = case blk.arity when 0 then blk[] when 1 then blk[params] else blk[*[params, env]] end case when when when when else end end
result NilClass then new(Merb::Request.new(env)) Hash, Mash then new(Merb::Request.new(result)) Merb::Request then new(result) Merb::Controller then result raise ArgumentError, "..."
kontroller.force_cache! kontroller._dispatch(action)
From the Library of Shirong Chen
12.3
Caching helpers
277
kontroller end
12.3.3 Fragment caching Fragment caching through the method fetch_fragment allows application developers to store small sections of a view template. This is perfect if only a region of a template needs caching. Friend
Looking into the source, we see how it magically identifies fragments by file and line number: def fetch_fragment(opts = {}, conditions = {}, &proc) if opts[:cache_key].blank? file, line = proc.to_s. scan(%r{ˆ#$}).first fragment_key = "#{file}[#{line}]" else fragment_key = opts.delete(:cache_key) end concat(Merb::Cache[_lookup_store(conditions)].fetch( fragment_key, opts, conditions) { capture(&proc) }, proc.binding) end
12.3.4 Partial caching The method fetch_partial can act as a replacement for partial when the partial needs to be cached. As we saw with fetch, the cached element is generally either read or otherwise written. However, unlike fetch, the fact that we’re referring to a template means there is no need for passing in a block. fetch_partial 'users/stats', :user => @user
From the Library of Shirong Chen
278
Chapter 12: Caching
Here’s the source behind fetch_partial; note how it takes a possible third parameter of conditions that will be passed along to the actual fetch method. def fetch_partial(template, opts={}, conditions = {}) template_id = template.to_s if template_id =˜ %r{ˆ/} template_path = File.dirname(template_id) / "_#{File.basename(template_id)}" else kontroller = (m = template_id.match(/.*(?=\/)/)) ? m[0] : controller_name template_id = "_#{File.basename(template_id)}" end unused, template_key = _template_for(template_id, opts.delete(:format) || content_type, kontroller, template_path) fetch_proc = lambda { partial(template, opts) } concat(Merb::Cache[_lookup_store(conditions)]. fetch(template_key, opts, conditions, &fetch_proc), fetch_proc.binding) end
12.4 Conclusion Caching in Merb can be handled in a number of ways. The best ways, however, are those that interfere the least with application code. The code behind action and eager caching exhibits the Merb controller’s ease in layering these mechanisms. Our look at the various caching stores available within Merb should make it clear that no one storage mechanism fits all, and that caching must make sense for the particular data being cached. Nonetheless, the caching store strategies provide a number of extensions on the abstract stores and when needed can be used to create composite user-defined stores as well.
From the Library of Shirong Chen
C HAPTER 13
Testing
Testing is critical to the robustness and longevity of your application, but unless you make it part of your application development process, you’re bound to find it painful. Somewhat surprisingly, however, teams and individuals who do integrate testing as part of standard development practices find that they almost can’t live without it. Why? There are two reasons. First, testing itself demands testable code, which in turn implies betterstructured code because of the need for cleanly interfacing units. Second, a well-built test suite is essentially machine-interpretable documentation that not only prevents regressions but also gives developers insight into the intent of the code, no matter its condition. Merb, as a framework and application development platform, has always encouraged testing. However, the specifics of testing continue to be intensely debated. For this reason, Merb has attempted to remain agnostic toward the tools used in building test suites. Nonetheless, a decision had to be made, and RSpec was picked both internally and at a stack level as the testing framework of choice. In this chapter we will cover the use of RSpec for testing models and requests, but following the precedent set by all the other chapters we’re more intent on revealing how testing has been integrated within the Merb framework than describing its precise use. That said, let’s start off with an exploration of tasks most commonly used with Merb when testing.
13.1 Rake tasks All rake tasks related to testing within Merb are contained within the task namespace :spec. Here’s the beginning of spectasks.rb contained within the Merb core: desc "Run specs, run a specific spec with "+ "TASK=spec/path_to_spec.rb" task :spec => [ "spec:default" ]
279 From the Library of Shirong Chen
280
Chapter 13: Testing
namespace :spec do OPTS_FILENAME = "./spec/spec.opts" if File.exist?(OPTS_FILENAME) SPEC_OPTS = ["--options", OPTS_FILENAME] else SPEC_OPTS = ["--color", "--format", "specdoc"] end Spec::Rake::SpecTask.new('default') do |t| t.spec_opts = SPEC_OPTS if(ENV['TASK']) t.spec_files = [ENV['TASK']] else t.spec_files = Dir['spec/**/*_spec.rb'].sort end end
Observe that the default spec task is effectively created by passing in the string 'default' to Spec::Rake::SpecTask.new. The SpecTask class has been designed
to simplify RSpec rake task creation. The default Merb spec, furthermore, sets two configuration variables through the block passed in to the initializer. These are the spec options and the spec files. Application developers should be aware that appending TASK='spec/some_spec.rb' to the rake spec command runs only one particular spec file. Otherwise, all files ending in _spec.rb and contained within any level subdirectory of spec/ are used. desc "Run all model specs, run a spec for a specific "+ "Model with MODEL=MyModel" Spec::Rake::SpecTask.new('model') do |t| t.spec_opts = SPEC_OPTS if(ENV['MODEL']) t.spec_files = Dir["spec/models/**/"#{ENV['MODEL']}_spec.rb"].sort else t.spec_files = Dir['spec/models/**/*_spec.rb'].sort end end desc "Run all request specs, run a spec for a specific "+ "Request with REQUEST=MyRequest" Spec::Rake::SpecTask.new('request') do |t| t.spec_opts = SPEC_OPTS if(ENV['REQUEST']) t.spec_files = Dir[
From the Library of Shirong Chen
13.1
Rake tasks
281
"spec/requests/**/#{ENV['REQUEST']}_spec.rb"].sort else t.spec_files = Dir['spec/requests/**/*_spec.rb'].sort end end # ...
The two non-default tasks, model and request, are meant to handle model and request specs. Their rake task construct is not very different from the default task, but developers should remember that MODEL and REQUEST are the environment variables for specifying the running of only particular spec files. Merb also creates a couple more tasks for view and controller testing, but most Merb developers frown upon the use of view and controller testing. The reasons are different in each case. View testing tends to be the testing of bluntly self-descriptive templates, whereas controller testing is a form of unit testing that, removed from the simulation of the request-response cycle, does not test context-genuine behavior. If you find yourself asking why the testing of context-genuine behavior is so important, you’ll definitely raise a substantially contentious question. Leaving opinion aside, the findings of many have been that the development of a test suite that does test context-genuine behavior alone quickly becomes a crufty mess whenever application code refactoring is attempted. Said differently, the isolation of particular application code through tests via excessive stubbing and mocking tends to reveal tests that possess uncanny knowledge of implementation and not simply interface. Our advice is consequently that you stick to the bread and butter of model and request specs, and if you truly think you need a controller spec, then you should probably refactor some code out of your indubitably overburdened controller. desc "Run all specs and output the result in html" Spec::Rake::SpecTask.new('html') do |t| t.spec_opts = ["--format", "html"] t.libs = ['lib', 'server/lib' ] t.spec_files = Dir['spec/**/*_spec.rb'].sort end desc "Run specs and check coverage with rcov" Spec::Rake::SpecTask.new('coverage') do |t| t.spec_opts = SPEC_OPTS t.spec_files = Dir['spec/**/*_spec.rb'].sort t.libs = ['lib', 'server/lib' ] t.rcov = true t.rcov_opts = ["--exclude 'config,spec,#{Gem::path.join(',')}'"] end end
From the Library of Shirong Chen
282
Chapter 13: Testing
Two other spec tasks exists. These are html and coverage. The former simply outputs spec results in HTML, and the other uses rcov to determine the coverage of the test suite. Rake task in hand, let’s move on to producing some actual spec files within a Merb application.
13.2 Spec files For the rest of this chapter we’re going to be working with a fake application called 0g. Honestly, though, the only critical bit of information about the application is that there is a Ship model and a Ships controller. To create the files for both of these classes, we used merb-gen resource ship. The only interesting bit is that it also gave us two additional files: spec/models/ship_spec.rb spec/requests/ships_spec.rb
Before we jump in and manipulate these spec files, let’s first look at the code for a file that both require, spec/spec_helper.rb: require "rubygems" # so local gems get priority if (local_gem_dir = File.join(File.dirname(_ _FILE_ _), '..', 'gems')) && $BUNDLE.nil? $BUNDLE = true Gem.clear_paths ; Gem.path.unshift(local_gem_dir) end require "merb-core" require "spec" # for those not using the rake tasks Merb.start_environment( :testing => true, :adapter => 'runner', :environment => ENV['MERB_ENV'] || 'test') Spec::Runner.configure do |config| config.include(Merb::Test::ViewHelper) config.include(Merb::Test::RouteHelper) config.include(Merb::Test::ControllerHelper)
From the Library of Shirong Chen
13.3
Model specs
283
config.before(:all) do DataMapper.auto_migrate! if Merb.orm == :datamapper end end
The most important method here is Merb.start_environment, which starts up Merb if it has not yet been started. Note, however, that it has been requested that Merb be started in testing mode. This prevents any Merb workers from being created, and though the application will be up and running, it won’t be accessible from any local port. Also note the inclusion of the test helper modules within the Spec::Runner.configure blocks; this makes them available for all the tests.
13.3 Model specs The spec file for our ship model is located at spec/models/ship_spec.rb. Without any modification it appears as shown below. require File.join( File.dirname(_ _FILE_ _), '..', "spec_helper" ) describe Ship do it "should have specs" end
Because the first spec description has egged us on, we should probably flesh out the file: before :each do Ship.auto_migrate! @s = Ship.new(:name => 'Starlight', :base_mass => 100, :fuel => 1000) end it "should have a various properties" do @s.should respond_to :name @s.should respond_to :base_mass @s.should respond_to :cargo_mass @s.should respond_to :fuel @s.should respond_to :x @s.should respond_to :y end
From the Library of Shirong Chen
284
Chapter 13: Testing
Through the specifications above we have set expectations about how a ship should behave. The methods before, it, and should are all out of the scope of this chapter, but if you need more familiarity with them please check the RSpec documentation. Let’s run our model specs and the result: $ rake spec:model (in /home/foysavas/src/0g) Loading init file from ˜/src/0g/config/init.rb Loading ˜/src/0g/config/environments/development.rb F 1) ArgumentError in 'Ship should have a various properties' The property 'base_mass' is not a public property. ./spec/models/ship_spec.rb:7:in ‘new' ./spec/models/ship_spec.rb:7: ...
Obviously, the code has not written itself, and so we have ended up with a failure. Let’s modify our model as follows: class Ship include DataMapper::Resource property property property property property property property
:id, Serial :name, String, :nullable => false :base_mass, Integer, :default => 0 :cargo_mass, Integer, :default => 0 :fuel, Integer, :default => 0 :x, Integer, :default => 0 :y, Integer, :default => 0
end
Now, running the model specs again, our specifications pass: $ rake spec:model (in /home/foysavas/src/0g) Loading init file from ˜/src/0g/config/init.rb Loading ˜/0g/config/environments/development.rb . Finished in 0.041148 seconds 1 example, 0 failures
From the Library of Shirong Chen
13.4
Request specs
285
Realistically, this has been only a taste of what model testing is like, but given that domain-level testing is more dependent upon knowing the chosen testing framework than any extensions that may be built on top of it (Merb in fact comes with no explicit model test helpers), we’ll just leave it at that.
13.4 Request specs Request specifications are the preferred way to test a Merb application’s full stack behavior. The concept, simple enough, is to fake a request in, and then set expectations on the response. Let’s open up the file controller spec file for ships. Since Ships was generated as a resource controller, a number of specifications establishing the basis of a RESTful interface are automatically provided: require File.join(File.dirname(_ _FILE_ _), '..', 'spec_helper.rb') given "a ship exists" do Ship.all.destroy! request(resource(:ships), :method => "POST", :params => { :ship => { :id => nil }}) end describe "resource(:ships)" do describe "GET" do before(:each) do @response = request(resource(:ships)) end it "responds successfully" do @response.should be_successful end
If you’re new to specs with Merb, the given will throw you off. This method, an extension to RSpec, takes a block that can be run before spec groups. We’ll see this usage later on, but for now, just recognize that it’s a great way to keep our tests DRY. Also notice the use of the method request, which creates a request and sends it to the Merb test server. Its first parameter is a URL, here given using the resource URL helper. The second and optional parameter is a hash of request parameters. Above, the request method and request params have been specified for the creation of a ship. Notice how the describes blocks are nested one inside the other. The first is for resource(:ships) and the second more specifically for how it behaves with GET applied.
From the Library of Shirong Chen
286
Chapter 13: Testing
There’s nothing magical about these names, and as we can see in the before(:each) block, a generic request to '/ships' is made. The return value of the request is saved as an instance variable so that we can set expectations on it within the individual it blocks. The first of these tests the most basic but essential expectation of the request: It should be successful; that is, it should come back to us with a successful status code without having raised any errors. it "contains a list of ships" do pending @response.should have_xpath("//ul") end end describe "GET", :given => "a ship exists" do before(:each) do @response = request(resource(:ships)) end it "has a list of ships" do pending @response.should have_xpath("//ul/li") end end
The next it block does something more specific: It tests that the response contains a list of ships. To do this, it makes use of the have_xpath helper, which tests for the presence of an element given an XPath. Now because the application may not respond with a list (maybe you need a table instead, or just a bunch of divs) and because a more thorough testing of the response is desirable, the pending method has been used to alert us to its incompleteness. The second describe block comes with a given that will run the previously described given block before each contained example. Otherwise the difference between this describe and the other are minuscule—a list element is also expected now that we know one ship is around. Having tested essentially the index action, let’s move on to more curious HTTP methods. describe "a successful POST" do before(:each) do Ship.all.destroy! @response = request(resource(:ships), :method => "POST", :params => { :ship => { :id => nil }}) end
From the Library of Shirong Chen
13.4
Request specs
287
it "redirects to resource(:ships)" do @response.should redirect_to(resource(Ship.first), :message => {:notice => "ship was successfully created"}) end end end describe "resource(@ship)" do describe "a successful DELETE", :given => "a ship exists" do before(:each) do @response = request(resource(Ship.first), :method => "DELETE") end it "should redirect to the index action" do @response.should redirect_to(resource(:ships)) end end end
As we see, POST and DELETE are used to send out requests to different URLs. The first goes to '/ships' and the second presumably goes to '/ships/1'. Both examples, however, set the expectation that we are redirected to some other URL using the method redirect_to. The great thing about RSpec and Ruby testing frameworks in general is that they read so naturally that they just make sense. describe "resource(:ships, :new)" do before(:each) do @response = request(resource(:ships, :new)) end it "responds successfully" do @response.should be_successful end end describe "resource(@ship, :edit)", :given => "a ship exists" do before(:each) do @response = request(resource(Ship.first, :edit))
From the Library of Shirong Chen
288
Chapter 13: Testing
end it "responds successfully" do @response.should be_successful end end
The two example groups, for /ships/new and /ships/1/edit, are barely filled in. When you get down to specifying your own application, you may want to make sure particular form elements exist on each of these pages. describe "resource(@ship)", :given => "a ship exists" do describe "GET" do before(:each) do @response = request(resource(Ship.first)) end it "responds successfully" do @response.should be_successful end end describe "PUT" do before(:each) do @ship = Ship.first @response = request(resource(@ship), :method => "PUT", :params => { :ship => {:id => @ship.id} }) end it "redirect to the ship show action" do @response.should redirect_to(resource(@ship)) end end end
Closing up the resource-generated request spec file, we see the testing of /ships/1. The first describe block tests the success of the retrieval of the page, and the second makes sure that updating the ship object redirects us to its page. For the rest of this chapter we will describe in detail the Merb test helper methods, some of which were shown above.
From the Library of Shirong Chen
13.5
Request helper
289
13.5 Request helper The request method which we saw before takes two parameters, the first a URL and the second an optional hash of all the environment variables related to the request. Let’s open up the source to see how it works: def request(uri, uri = url(uri) uri = URI(uri) uri.scheme ||= uri.host ||=
env = {}) if uri.is_a?(Symbol) "http" "example.org"
if (env[:method] == "POST" || env["REQUEST_METHOD"] == "POST") params = env.delete(:body_params) if env.key?(:body_params) params = env.delete(:params) if env.key?(:params) && !env.key?(:input) unless env.key?(:input) env[:input] = Merb::Parse.params_to_query_string(params) env["CONTENT_TYPE"] = "application/x-www-form-urlencoded" end end if env[:params] uri.query = [ uri.query, Merb::Parse.params_to_query_string( env.delete(:params)) ].compact.join("&") end
Above we see the request method taking the uri given and translating it to the full and proper uri to test with. Note that we can pass in a symbol and it is automatically wrapped in the method url. We advise against this, however, for the sake of explicitness within specs. If the URL method is POST, then the method pushes all the params as a query string into env[:input]. We can do this ourselves, test-side, using the hash option :input, but there are few cases when it’s desirable to do so. Finally, if the request method is not POST, all params are inserted as query params.
From the Library of Shirong Chen
290
Chapter 13: Testing
ignore_cookies = env.has_key?(:jar) && env[:jar].nil? unless ignore_cookies @_ _cookie_jar_ _ ||= Merb::Test::CookieJar.new jar = env.delete(:jar) || :default @_ _cookie_jar_ _.update(jar, uri, env.delete(:cookie)) if env.has_key?(:cookie) env["HTTP_COOKIE"] = @_ _cookie_jar_ _.for(jar, uri) end
The request method also makes sending and receiving cookies a possibility. To do so we can pass in a cookie jar with the option :jar. This jar must be constructed using the class Merb::Test::CookieJar. Because statefulness through cookies is so important on the web, if we do not pass in a cookie jar a default one is passed in for us. app = Merb::Config[:app] rack = app.call(::Rack::MockRequest.env_for(uri.to_s, env)) rack = Struct.new(:status, :headers, :body, :url, :original_env). new(rack[0], rack[1], rack[2], uri.to_s, env) @_ _cookie_jar_ _.update(jar, uri, rack.headers["Set-Cookie"]) unless ignore_cookies Merb::Dispatcher.work_queue.size.times do Merb::Dispatcher.work_queue.pop.call end rack end end
And here’s the real magic. Using Rack::MockRequest, the request is sent off and handled by our application. Note how the rack response is restructured for our ease in testing. The methods status, helpers, body, url, and original_env all get us to the bits and pieces of the response we want to test. Finally, the dispatcher work queue is emptied, so we can have a clean room for testing our next request no matter what happened.
13.6 Request matchers Among the spec matchers Merb provides, several simply test the application’s responsiveness to the request. We’ve already seen a few of them, so now let’s peek inside to see what they’re made of: Spec::Matchers.create( :be_successful, :respond_successfully) do
From the Library of Shirong Chen
13.6
Request matchers
291
matches do |rack| @status = rack.respond_to?(:status) ? rack.status : rack @inspect = describe_input(rack) (200..207).include?(@status) end message do |not_string, rack| if @inspect.is_a?(Numeric) "Expected status code#{not_string} to be successful" else "Expected #{@inspect}#{not_string} " \ "to be successful, but it returned a #{@status}" end end end
Created via the class method Spec::Matchers.create (a method Merb provides as an extension to RSpec), be_successful and respond_successfully are two matchers that test if the status code returned was somewhere in the range of 200 to 207. Note the use of the message to return failure messages. Spec::Matchers.create(:be_missing, :be_client_error) do matches do |rack| @status = rack.respond_to?(:status) ? rack.status : rack @inspect = describe_input(rack) (400..417).include?(@status) end message do |not_string, rack| unless @inspect.is_a?(Numeric) "Expected #{@inspect}#{not_string} " \ "to be missing, but it returned a #{@status}" else "Expected #{not_string ? "not to get " : ""}"\ "a missing error code, " \ "but got #{@inspect}" end end end
The matchers be_missing and be_client_error are just the same, but for status codes 400 through 417.
From the Library of Shirong Chen
292
Chapter 13: Testing
Spec::Matchers.create(:have_body) do matches do |rack, body| @actual = if rack.respond_to?(:body) rack.body.to_s else rack.to_s end @actual == body end negative_failure_message do |rack, body| "Expected the response not to match:\n"\ #{body}\nActual response was:\n #{@actual}" end failure_message do |rack, body| "Expected the response to match:\n"\ #{body}\nActual response was:\n #{@actual}" end end
The match have_body is expected to be used with both should and should_not. As a consequence, its failure messages are bifurcated into failure message and negative failure message. Additionally, note the second block parameter with matches. This is the parameter on the have_body matcher method. If they do not match (or, in the negative case, do match), then the test will fail. The code for the rest of the matchers is very similar to what we’ve already seen, so we’ll leave you to venture into it if you so desire. The remaining request matchers are of use to application developers, however, so let’s describe them more briefly. The match have_content_type takes in a MIME symbol (for example, :html) that the request should match. Finally, the matchers redirect and redirect_to test whether a response has resulted in redirect and, in the case of redirect_to, if it was redirected to a specific location.
13.7 RSpec extensions There are a few helper extensions Merb makes to RSpec directly. Let’s take a look at one we’ve already encountered. Remember the given method from the request specs? Here it is: require 'spec' module Kernel
From the Library of Shirong Chen
13.7
RSpec extensions
293
def given(*args, &example_group_block) args