| 1 | # == About camping.rb |
|---|
| 2 | # |
|---|
| 3 | # Camping comes with two versions of its source code. The code contained in |
|---|
| 4 | # lib/camping.rb is compressed, stripped of whitespace, using compact algorithms |
|---|
| 5 | # to keep it tight. The unspoken rule is that camping.rb should be flowed with |
|---|
| 6 | # no more than 80 characters per line and must not exceed four kilobytes. |
|---|
| 7 | # |
|---|
| 8 | # On the other hand, lib/camping-unabridged.rb contains the same code, laid out |
|---|
| 9 | # nicely with piles of documentation everywhere. This documentation is entirely |
|---|
| 10 | # generated from lib/camping-unabridged.rb using RDoc and our "flipbook" template |
|---|
| 11 | # found in the extras directory of any camping distribution. |
|---|
| 12 | # |
|---|
| 13 | # == Requirements |
|---|
| 14 | # |
|---|
| 15 | # Camping requires at least Ruby 1.8.2. |
|---|
| 16 | # |
|---|
| 17 | # Camping depends on the following libraries. If you install through RubyGems, |
|---|
| 18 | # these will be automatically installed for you. |
|---|
| 19 | # |
|---|
| 20 | # * ActiveRecord, used in your models. |
|---|
| 21 | # ActiveRecord is an object-to-relational database mapper with adapters |
|---|
| 22 | # for SQLite3, MySQL, PostgreSQL, SQL Server and more. |
|---|
| 23 | # * Markaby, used in your views to describe HTML in plain Ruby. |
|---|
| 24 | # * MetAid, a few metaprogramming methods which Camping uses. |
|---|
| 25 | # * Tempfile, for storing file uploads. |
|---|
| 26 | # |
|---|
| 27 | # Camping also works well with Mongrel, the swift Ruby web server. |
|---|
| 28 | # http://rubyforge.org/projects/mongrel Mongrel comes with examples |
|---|
| 29 | # in its <tt>examples/camping</tt> directory. |
|---|
| 30 | # |
|---|
| 31 | %w[tempfile uri].map { |l| require l } |
|---|
| 32 | |
|---|
| 33 | class Object #:nodoc: |
|---|
| 34 | def meta_def(m,&b) #:nodoc: |
|---|
| 35 | (class<<self;self end).send(:define_method,m,&b) |
|---|
| 36 | end |
|---|
| 37 | end |
|---|
| 38 | |
|---|
| 39 | # == Camping |
|---|
| 40 | # |
|---|
| 41 | # The camping module contains three modules for separating your application: |
|---|
| 42 | # |
|---|
| 43 | # * Camping::Models for your database interaction classes, all derived from ActiveRecord::Base. |
|---|
| 44 | # * Camping::Controllers for storing controller classes, which map URLs to code. |
|---|
| 45 | # * Camping::Views for storing methods which generate HTML. |
|---|
| 46 | # |
|---|
| 47 | # Of use to you is also one module for storing helpful additional methods: |
|---|
| 48 | # |
|---|
| 49 | # * Camping::Helpers which can be used in controllers and views. |
|---|
| 50 | # |
|---|
| 51 | # == The Camping Server |
|---|
| 52 | # |
|---|
| 53 | # How do you run Camping apps? Oh, uh... The Camping Server! |
|---|
| 54 | # |
|---|
| 55 | # The Camping Server is, firstly and thusly, a set of rules. At the very least, The Camping Server must: |
|---|
| 56 | # |
|---|
| 57 | # * Load all Camping apps in a directory. |
|---|
| 58 | # * Load new apps that appear in that directory. |
|---|
| 59 | # * Mount those apps according to their filename. (e.g. blog.rb is mounted at /blog.) |
|---|
| 60 | # * Run each app's <tt>create</tt> method upon startup. |
|---|
| 61 | # * Reload the app if its modification time changes. |
|---|
| 62 | # * Reload the app if it requires any files under the same directory and one of their modification times changes. |
|---|
| 63 | # * Support the X-Sendfile header. |
|---|
| 64 | # |
|---|
| 65 | # In fact, Camping comes with its own little The Camping Server. |
|---|
| 66 | # |
|---|
| 67 | # At a command prompt, run: <tt>camping examples/</tt> and the entire <tt>examples/</tt> directory will be served. |
|---|
| 68 | # |
|---|
| 69 | # Configurations also exist for Apache and Lighttpd. See http://code.whytheluckystiff.net/camping/wiki/TheCampingServer. |
|---|
| 70 | # |
|---|
| 71 | # == The <tt>create</tt> method |
|---|
| 72 | # |
|---|
| 73 | # Many postambles will check for your application's <tt>create</tt> method and will run it |
|---|
| 74 | # when the web server starts up. This is a good place to check for database tables and create |
|---|
| 75 | # those tables to save users of your application from needing to manually set them up. |
|---|
| 76 | # |
|---|
| 77 | # def Blog.create |
|---|
| 78 | # unless Blog::Models::Post.table_exists? |
|---|
| 79 | # ActiveRecord::Schema.define do |
|---|
| 80 | # create_table :blog_posts, :force => true do |t| |
|---|
| 81 | # t.column :user_id, :integer, :null => false |
|---|
| 82 | # t.column :title, :string, :limit => 255 |
|---|
| 83 | # t.column :body, :text |
|---|
| 84 | # end |
|---|
| 85 | # end |
|---|
| 86 | # end |
|---|
| 87 | # end |
|---|
| 88 | # |
|---|
| 89 | # For more tips, see http://code.whytheluckystiff.net/camping/wiki/GiveUsTheCreateMethod. |
|---|
| 90 | module Camping |
|---|
| 91 | C = self |
|---|
| 92 | S = IO.read(__FILE__) rescue nil |
|---|
| 93 | P = '<h1>Cam\ping Problem!</h1><h2>%s</h2>' |
|---|
| 94 | # An object-like Hash. |
|---|
| 95 | # All Camping query string and cookie variables are loaded as this. |
|---|
| 96 | # |
|---|
| 97 | # To access the query string, for instance, use the <tt>@input</tt> variable. |
|---|
| 98 | # |
|---|
| 99 | # module Blog::Controllers |
|---|
| 100 | # class Index < R '/' |
|---|
| 101 | # def get |
|---|
| 102 | # if page = @input.page.to_i > 0 |
|---|
| 103 | # page -= 1 |
|---|
| 104 | # end |
|---|
| 105 | # @posts = Post.find :all, :offset => page * 20, :limit => 20 |
|---|
| 106 | # render :index |
|---|
| 107 | # end |
|---|
| 108 | # end |
|---|
| 109 | # end |
|---|
| 110 | # |
|---|
| 111 | # In the above example if you visit <tt>/?page=2</tt>, you'll get the second |
|---|
| 112 | # page of twenty posts. You can also use <tt>@input[:page]</tt> or <tt>@input['page']</tt> |
|---|
| 113 | # to get the value for the <tt>page</tt> query variable. |
|---|
| 114 | # |
|---|
| 115 | # Use the <tt>@cookies</tt> variable in the same fashion to access cookie variables. |
|---|
| 116 | # Also, the <tt>@env</tt> variable is an H containing the HTTP headers and server info. |
|---|
| 117 | class H < Hash |
|---|
| 118 | # Gets or sets keys in the hash. |
|---|
| 119 | # |
|---|
| 120 | # @cookies.my_favorite = :macadamian |
|---|
| 121 | # @cookies.my_favorite |
|---|
| 122 | # => :macadamian |
|---|
| 123 | # |
|---|
| 124 | def method_missing(m,*a) |
|---|
| 125 | m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.to_s]:super |
|---|
| 126 | end |
|---|
| 127 | alias u merge! |
|---|
| 128 | undef id, type |
|---|
| 129 | end |
|---|
| 130 | |
|---|
| 131 | # Helpers contains methods available in your controllers and views. You may add |
|---|
| 132 | # methods of your own to this module, including many helper methods from Rails. |
|---|
| 133 | # This is analogous to Rails' <tt>ApplicationHelper</tt> module. |
|---|
| 134 | # |
|---|
| 135 | # == Using ActionPack Helpers |
|---|
| 136 | # |
|---|
| 137 | # If you'd like to include helpers from Rails' modules, you'll need to look up the |
|---|
| 138 | # helper module in the Rails documentation at http://api.rubyonrails.org/. |
|---|
| 139 | # |
|---|
| 140 | # For example, if you look up the <tt>ActionView::Helpers::FormHelper</tt> class, |
|---|
| 141 | # you'll find that it's loaded from the <tt>action_view/helpers/form_helper.rb</tt> |
|---|
| 142 | # file. You'll need to have the ActionPack gem installed for this to work. |
|---|
| 143 | # |
|---|
| 144 | # require 'action_view/helpers/form_helper.rb' |
|---|
| 145 | # |
|---|
| 146 | # # This example is unfinished.. soon.. |
|---|
| 147 | # |
|---|
| 148 | module Helpers |
|---|
| 149 | # From inside your controllers and views, you will often need to figure out |
|---|
| 150 | # the route used to get to a certain controller +c+. Pass the controller class |
|---|
| 151 | # and any arguments into the R method, a string containing the route will be |
|---|
| 152 | # returned to you. |
|---|
| 153 | # |
|---|
| 154 | # Assuming you have a specific route in an edit controller: |
|---|
| 155 | # |
|---|
| 156 | # class Edit < R '/edit/(\d+)' |
|---|
| 157 | # |
|---|
| 158 | # A specific route to the Edit controller can be built with: |
|---|
| 159 | # |
|---|
| 160 | # R(Edit, 1) |
|---|
| 161 | # |
|---|
| 162 | # Which outputs: <tt>/edit/1</tt>. |
|---|
| 163 | # |
|---|
| 164 | # You may also pass in a model object and the ID of the object will be used. |
|---|
| 165 | # |
|---|
| 166 | # If a controller has many routes, the route will be selected if it is the |
|---|
| 167 | # first in the routing list to have the right number of arguments. |
|---|
| 168 | # |
|---|
| 169 | # == Using R in the View |
|---|
| 170 | # |
|---|
| 171 | # Keep in mind that this route doesn't include the root path. |
|---|
| 172 | # You will need to use <tt>/</tt> (the slash method above) in your controllers. |
|---|
| 173 | # Or, go ahead and use the Helpers#URL method to build a complete URL for a route. |
|---|
| 174 | # |
|---|
| 175 | # However, in your views, the :href, :src and :action attributes automatically |
|---|
| 176 | # pass through the slash method, so you are encouraged to use <tt>R</tt> or |
|---|
| 177 | # <tt>URL</tt> in your views. |
|---|
| 178 | # |
|---|
| 179 | # module Blog::Views |
|---|
| 180 | # def menu |
|---|
| 181 | # div.menu! do |
|---|
| 182 | # a 'Home', :href => URL() |
|---|
| 183 | # a 'Profile', :href => "/profile" |
|---|
| 184 | # a 'Logout', :href => R(Logout) |
|---|
| 185 | # a 'Google', :href => 'http://google.com' |
|---|
| 186 | # end |
|---|
| 187 | # end |
|---|
| 188 | # end |
|---|
| 189 | # |
|---|
| 190 | # Let's say the above example takes place inside an application mounted at |
|---|
| 191 | # <tt>http://localhost:3301/frodo</tt> and that a controller named <tt>Logout</tt> |
|---|
| 192 | # is assigned to route <tt>/logout</tt>. The HTML will come out as: |
|---|
| 193 | # |
|---|
| 194 | # <div id="menu"> |
|---|
| 195 | # <a href="//localhost:3301/frodo/">Home</a> |
|---|
| 196 | # <a href="/frodo/profile">Profile</a> |
|---|
| 197 | # <a href="/frodo/logout">Logout</a> |
|---|
| 198 | # <a href="http://google.com">Google</a> |
|---|
| 199 | # </div> |
|---|
| 200 | # |
|---|
| 201 | def R(c,*g) |
|---|
| 202 | p,h=/\(.+?\)/,g.grep(Hash) |
|---|
| 203 | g-=h |
|---|
| 204 | raise "bad route" unless u = c.urls.find{|x| |
|---|
| 205 | break x if x.scan(p).size == g.size && |
|---|
| 206 | /^#{x}\/?$/ =~ (x=g.inject(x){|x,a| |
|---|
| 207 | x.sub p,C.escape((a[a.class.primary_key]rescue a))}) |
|---|
| 208 | } |
|---|
| 209 | h.any?? u+"?"+h[0].map{|x|x.map{|z|C.escape z}*"="}*"&": u |
|---|
| 210 | end |
|---|
| 211 | |
|---|
| 212 | # Simply builds a complete path from a path +p+ within the app. If your application is |
|---|
| 213 | # mounted at <tt>/blog</tt>: |
|---|
| 214 | # |
|---|
| 215 | # self / "/view/1" #=> "/blog/view/1" |
|---|
| 216 | # self / "styles.css" #=> "styles.css" |
|---|
| 217 | # self / R(Edit, 1) #=> "/blog/edit/1" |
|---|
| 218 | # |
|---|
| 219 | def /(p); p[/^\//]?@root+p:p end |
|---|
| 220 | # Builds a URL route to a controller or a path, returning a URI object. |
|---|
| 221 | # This way you'll get the hostname and the port number, a complete URL. |
|---|
| 222 | # No scheme is given (http or https). |
|---|
| 223 | # |
|---|
| 224 | # You can use this to grab URLs for controllers using the R-style syntax. |
|---|
| 225 | # So, if your application is mounted at <tt>http://test.ing/blog/</tt> |
|---|
| 226 | # and you have a View controller which routes as <tt>R '/view/(\d+)'</tt>: |
|---|
| 227 | # |
|---|
| 228 | # URL(View, @post.id) #=> #<URL://test.ing/blog/view/12> |
|---|
| 229 | # |
|---|
| 230 | # Or you can use the direct path: |
|---|
| 231 | # |
|---|
| 232 | # self.URL #=> #<URL://test.ing/blog/> |
|---|
| 233 | # self.URL + "view/12" #=> #<URL://test.ing/blog/view/12> |
|---|
| 234 | # URL("/view/12") #=> #<URL://test.ing/blog/view/12> |
|---|
| 235 | # |
|---|
| 236 | # Since no scheme is given, you will need to add the scheme yourself: |
|---|
| 237 | # |
|---|
| 238 | # "http" + URL("/view/12") #=> "http://test.ing/blog/view/12" |
|---|
| 239 | # |
|---|
| 240 | # It's okay to pass URL strings through this method as well: |
|---|
| 241 | # |
|---|
| 242 | # URL("http://google.com") #=> #<URI:http://google.com> |
|---|
| 243 | # |
|---|
| 244 | # Any string which doesn't begin with a slash will pass through |
|---|
| 245 | # unscathed. |
|---|
| 246 | def URL c='/',*a |
|---|
| 247 | c = R(c, *a) if c.respond_to? :urls |
|---|
| 248 | c = self/c |
|---|
| 249 | c = "//"+@env.HTTP_HOST+c if c[/^\//] |
|---|
| 250 | URI(c) |
|---|
| 251 | end |
|---|
| 252 | end |
|---|
| 253 | |
|---|
| 254 | # Camping::Base is built into each controller by way of the generic routing |
|---|
| 255 | # class Camping::R. In some ways, this class is trying to do too much, but |
|---|
| 256 | # it saves code for all the glue to stay in one place. |
|---|
| 257 | # |
|---|
| 258 | # Forgivable, considering that it's only really a handful of methods and accessors. |
|---|
| 259 | # |
|---|
| 260 | # == Treating controller methods like Response objects |
|---|
| 261 | # |
|---|
| 262 | # Camping originally came with a barebones Response object, but it's often much more readable |
|---|
| 263 | # to just use your controller as the response. |
|---|
| 264 | # |
|---|
| 265 | # Go ahead and alter the status, cookies, headers and body instance variables as you |
|---|
| 266 | # see fit in order to customize the response. |
|---|
| 267 | # |
|---|
| 268 | # module Camping::Controllers |
|---|
| 269 | # class SoftLink |
|---|
| 270 | # def get |
|---|
| 271 | # redirect "/" |
|---|
| 272 | # end |
|---|
| 273 | # end |
|---|
| 274 | # end |
|---|
| 275 | # |
|---|
| 276 | # Is equivalent to: |
|---|
| 277 | # |
|---|
| 278 | # module Camping::Controllers |
|---|
| 279 | # class SoftLink |
|---|
| 280 | # def get |
|---|
| 281 | # @status = 302 |
|---|
| 282 | # @headers['Location'] = "/" |
|---|
| 283 | # end |
|---|
| 284 | # end |
|---|
| 285 | # end |
|---|
| 286 | # |
|---|
| 287 | module Base |
|---|
| 288 | attr_accessor :input, :cookies, :env, :headers, :body, :status, :root |
|---|
| 289 | Z = "\r\n" |
|---|
| 290 | |
|---|
| 291 | # Display a view, calling it by its method name +m+. If a <tt>layout</tt> |
|---|
| 292 | # method is found in Camping::Views, it will be used to wrap the HTML. |
|---|
| 293 | # |
|---|
| 294 | # module Camping::Controllers |
|---|
| 295 | # class Show |
|---|
| 296 | # def get |
|---|
| 297 | # @posts = Post.find :all |
|---|
| 298 | # render :index |
|---|
| 299 | # end |
|---|
| 300 | # end |
|---|
| 301 | # end |
|---|
| 302 | # |
|---|
| 303 | def render(m); end; undef_method :render |
|---|
| 304 | |
|---|
| 305 | # Any stray method calls will be passed to Markaby. This means you can reply |
|---|
| 306 | # with HTML directly from your controller for quick debugging. |
|---|
| 307 | # |
|---|
| 308 | # module Camping::Controllers |
|---|
| 309 | # class Info |
|---|
| 310 | # def get; code @env.inspect end |
|---|
| 311 | # end |
|---|
| 312 | # end |
|---|
| 313 | # |
|---|
| 314 | # If you have a <tt>layout</tt> method in Camping::Views, it will be used to |
|---|
| 315 | # wrap the HTML. |
|---|
| 316 | def method_missing(*a,&b) |
|---|
| 317 | a.shift if a[0]==:render |
|---|
| 318 | m=Mab.new({},self) |
|---|
| 319 | s=m.capture{send(*a,&b)} |
|---|
| 320 | s=m.capture{send(:layout){s}} if /^_/!~a[0].to_s and m.respond_to?:layout |
|---|
| 321 | s |
|---|
| 322 | end |
|---|
| 323 | |
|---|
| 324 | # A quick means of setting this controller's status, body and headers. |
|---|
| 325 | # Used internally by Camping, but... by all means... |
|---|
| 326 | # |
|---|
| 327 | # r(302, '', 'Location' => self / "/view/12") |
|---|
| 328 | # |
|---|
| 329 | # Is equivalent to: |
|---|
| 330 | # |
|---|
| 331 | # redirect "/view/12" |
|---|
| 332 | # |
|---|
| 333 | # See also: #r404, #r500 and #r501 |
|---|
| 334 | def r(s, b, h = {}); @status = s; headers.u(h); @body = b; end |
|---|
| 335 | |
|---|
| 336 | # Formulate a redirect response: a 302 status with <tt>Location</tt> header |
|---|
| 337 | # and a blank body. Uses Helpers#URL to build the location from a controller |
|---|
| 338 | # route or path. |
|---|
| 339 | # |
|---|
| 340 | # So, given a root of <tt>http://localhost:3301/articles</tt>: |
|---|
| 341 | # |
|---|
| 342 | # redirect "view/12" # redirects to "//localhost:3301/articles/view/12" |
|---|
| 343 | # redirect View, 12 # redirects to "//localhost:3301/articles/view/12" |
|---|
| 344 | # |
|---|
| 345 | # <b>NOTE:</b> This method doesn't magically exit your methods and redirect. |
|---|
| 346 | # You'll need to <tt>return redirect(...)</tt> if this isn't the last statement |
|---|
| 347 | # in your code. |
|---|
| 348 | def redirect(*a) |
|---|
| 349 | r(302,'','Location'=>URL(*a)) |
|---|
| 350 | end |
|---|
| 351 | |
|---|
| 352 | # Called when a controller was not found. It is mainly used internally, but it can |
|---|
| 353 | # also be useful for you, if you want to filter some parameters. |
|---|
| 354 | # |
|---|
| 355 | # module Camping |
|---|
| 356 | # def r404(p=env.PATH) |
|---|
| 357 | # @status = 404 |
|---|
| 358 | # div do |
|---|
| 359 | # h1 'Camping Problem!' |
|---|
| 360 | # h2 "#{p} not found" |
|---|
| 361 | # end |
|---|
| 362 | # end |
|---|
| 363 | # end |
|---|
| 364 | # |
|---|
| 365 | # See: I |
|---|
| 366 | def r404(p=env.PATH) |
|---|
| 367 | r(404, P % "#{p} not found") |
|---|
| 368 | end |
|---|
| 369 | |
|---|
| 370 | # If there is a parse error in Camping or in your application's source code, it will not be caught |
|---|
| 371 | # by Camping. The controller class +k+ and request method +m+ (GET, POST, etc.) where the error |
|---|
| 372 | # took place are passed in, along with the Exception +e+ which can be mined for useful info. |
|---|
| 373 | # |
|---|
| 374 | # You can overide it, but if you have an error in here, it will be uncaught ! |
|---|
| 375 | # |
|---|
| 376 | # See: I |
|---|
| 377 | def r500(k,m,x) |
|---|
| 378 | r(500, P % "#{k}.#{m}" + "<h3>#{x.class} #{x.message}: <ul>#{x.backtrace.map{|b|"<li>#{b}</li>"}}</ul></h3>") |
|---|
| 379 | end |
|---|
| 380 | |
|---|
| 381 | # Called if an undefined method is called on a Controller, along with the request method +m+ (GET, POST, etc.) |
|---|
| 382 | # |
|---|
| 383 | # See: I |
|---|
| 384 | def r501(m=@method) |
|---|
| 385 | r(501, P % "#{m.upcase} not implemented") |
|---|
| 386 | end |
|---|
| 387 | |
|---|
| 388 | # Turn a controller into an array. This is designed to be used to pipe |
|---|
| 389 | # controllers into the <tt>r</tt> method. A great way to forward your |
|---|
| 390 | # requests! |
|---|
| 391 | # |
|---|
| 392 | # class Read < '/(\d+)' |
|---|
| 393 | # def get(id) |
|---|
| 394 | # Post.find(id) |
|---|
| 395 | # rescue |
|---|
| 396 | # r *Blog.get(:NotFound, @env.REQUEST_URI) |
|---|
| 397 | # end |
|---|
| 398 | # end |
|---|
| 399 | # |
|---|
| 400 | def to_a;[status, body, headers] end |
|---|
| 401 | |
|---|
| 402 | def initialize(r, e, m) #:nodoc: |
|---|
| 403 | @status, @method, @env, @headers, @root = 200, m, e, |
|---|
| 404 | H['Content-Type','text/html'], e.SCRIPT_NAME.sub(/\/$/,'') |
|---|
| 405 | @k = C.kp(e.HTTP_COOKIE) |
|---|
| 406 | q = C.qsp(e.QUERY_STRING) |
|---|
| 407 | @in = r |
|---|
| 408 | case e.CONTENT_TYPE |
|---|
| 409 | when %r|\Amultipart/form-.*boundary=\"?([^\";,]+)|n |
|---|
| 410 | b = /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/ |
|---|
| 411 | until @in.eof? |
|---|
| 412 | fh=H[] |
|---|
| 413 | for l in @in |
|---|
| 414 | case l |
|---|
| 415 | when Z: break |
|---|
| 416 | when /^Content-D.+?: form-data;/ |
|---|
| 417 | fh.u H[*$'.scan(/(?:\s(\w+)="([^"]+)")/).flatten] |
|---|
| 418 | when /^Content-Type: (.+?)(\r$|\Z)/m |
|---|
| 419 | fh.type = $1 |
|---|
| 420 | end |
|---|
| 421 | end |
|---|
| 422 | fn=fh.name |
|---|
| 423 | o=if fh.filename |
|---|
| 424 | o=fh.tempfile=Tempfile.new(:C) |
|---|
| 425 | o.binmode |
|---|
| 426 | else |
|---|
| 427 | fh="" |
|---|
| 428 | end |
|---|
| 429 | s=8192 |
|---|
| 430 | k='' |
|---|
| 431 | l=@in.read(s*2) |
|---|
| 432 | while l |
|---|
| 433 | if (k<<l)=~b |
|---|
| 434 | o<<$`.chomp |
|---|
| 435 | @in.seek(-$'.size,IO::SEEK_CUR) |
|---|
| 436 | break |
|---|
| 437 | end |
|---|
| 438 | o<<k.slice!(0...s) |
|---|
| 439 | l=@in.read(s) |
|---|
| 440 | end |
|---|
| 441 | C.qsp(fn,'&;',fh,q) if fn |
|---|
| 442 | fh.tempfile.rewind if fh.is_a?H |
|---|
| 443 | end |
|---|
| 444 | when "application/x-www-form-urlencoded" |
|---|
| 445 | q.u(C.qsp(@in.read)) |
|---|
| 446 | end |
|---|
| 447 | @cookies, @input = @k.dup, q.dup |
|---|
| 448 | end |
|---|
| 449 | |
|---|
| 450 | # All requests pass through this method before going to the controller. Some magic |
|---|
| 451 | # in Camping can be performed by overriding this method. |
|---|
| 452 | # |
|---|
| 453 | # See http://code.whytheluckystiff.net/camping/wiki/BeforeAndAfterOverrides for more |
|---|
| 454 | # on before and after overrides with Camping. |
|---|
| 455 | def service(*a) |
|---|
| 456 | @body = send(@method, *a) |
|---|
| 457 | headers['Set-Cookie'] = cookies.map { |k,v| "#{k}=#{C.escape(v)}; path=#{self/"/"}" if v != @k[k] } - [nil] |
|---|
| 458 | self |
|---|
| 459 | end |
|---|
| 460 | |
|---|
| 461 | # Used by the web server to convert the current request to a string. If you need to |
|---|
| 462 | # alter the way Camping builds HTTP headers, consider overriding this method. |
|---|
| 463 | def to_s |
|---|
| 464 | "Status: #@status#{Z+(headers.inject([]){|a,o|[*o[1]].map{|v|a<<[o[0],v]*": "if v&&v.to_s.any?};a}*Z)+Z+Z}#@body" |
|---|
| 465 | end |
|---|
| 466 | |
|---|
| 467 | end |
|---|
| 468 | |
|---|
| 469 | # Controllers is a module for placing classes which handle URLs. This is done |
|---|
| 470 | # by defining a route to each class using the Controllers::R method. |
|---|
| 471 | # |
|---|
| 472 | # module Camping::Controllers |
|---|
| 473 | # class Edit < R '/edit/(\d+)' |
|---|
| 474 | # def get; end |
|---|
| 475 | # def post; end |
|---|
| 476 | # end |
|---|
| 477 | # end |
|---|
| 478 | # |
|---|
| 479 | # If no route is set, Camping will guess the route from the class name. |
|---|
| 480 | # The rule is very simple: the route becomes a slash followed by the lowercased |
|---|
| 481 | # class name. See Controllers::D for the complete rules of dispatch. |
|---|
| 482 | # |
|---|
| 483 | # == Special classes |
|---|
| 484 | # |
|---|
| 485 | # There are two special classes used for handling 404 and 500 errors. The |
|---|
| 486 | # NotFound class handles URLs not found. The ServerError class handles exceptions |
|---|
| 487 | # uncaught by your application. |
|---|
| 488 | X = module Controllers |
|---|
| 489 | @r = [] |
|---|
| 490 | class << self |
|---|
| 491 | def r #:nodoc: |
|---|
| 492 | @r |
|---|
| 493 | end |
|---|
| 494 | # Add routes to a controller class by piling them into the R method. |
|---|
| 495 | # |
|---|
| 496 | # module Camping::Controllers |
|---|
| 497 | # class Edit < R '/edit/(\d+)', '/new' |
|---|
| 498 | # def get(id) |
|---|
| 499 | # if id # edit |
|---|
| 500 | # else # new |
|---|
| 501 | # end |
|---|
| 502 | # end |
|---|
| 503 | # end |
|---|
| 504 | # end |
|---|
| 505 | # |
|---|
| 506 | # You will need to use routes in either of these cases: |
|---|
| 507 | # |
|---|
| 508 | # * You want to assign multiple routes to a controller. |
|---|
| 509 | # * You want your controller to receive arguments. |
|---|
| 510 | # |
|---|
| 511 | # Most of the time the rules inferred by dispatch method Controllers::D will get you |
|---|
| 512 | # by just fine. |
|---|
| 513 | def R *u |
|---|
| 514 | r=@r |
|---|
| 515 | Class.new { |
|---|
| 516 | meta_def(:urls){u} |
|---|
| 517 | meta_def(:inherited){|x|r<<x} |
|---|
| 518 | } |
|---|
| 519 | end |
|---|
| 520 | |
|---|
| 521 | # Dispatch routes to controller classes. |
|---|
| 522 | # For each class, routes are checked for a match based on their order in the routing list |
|---|
| 523 | # given to Controllers::R. If no routes were given, the dispatcher uses a slash followed |
|---|
| 524 | # by the name of the controller lowercased. |
|---|
| 525 | # |
|---|
| 526 | # Controllers are searched in this order: |
|---|
| 527 | # |
|---|
| 528 | # # Classes without routes, since they refer to a very specific URL. |
|---|
| 529 | # # Classes with routes are searched in order of their creation. |
|---|
| 530 | # |
|---|
| 531 | # So, define your catch-all controllers last. |
|---|
| 532 | def D(p, m) |
|---|
| 533 | r.map { |k| |
|---|
| 534 | k.urls.map { |x| |
|---|
| 535 | return (k.instance_method(m) rescue nil) ? |
|---|
| 536 | [k, m, *$~[1..-1]] : [I, 'r501', m] if p =~ /^#{x}\/?$/ |
|---|
| 537 | } |
|---|
| 538 | } |
|---|
| 539 | [I, 'r404', p] |
|---|
| 540 | end |
|---|
| 541 | |
|---|
| 542 | # The route maker, this is called by Camping internally, you shouldn't need to call it. |
|---|
| 543 | # |
|---|
| 544 | # Still, it's worth know what this method does. Since Ruby doesn't keep track of class |
|---|
| 545 | # creation order, we're keeping an internal list of the controllers which inherit from R(). |
|---|
| 546 | # This method goes through and adds all the remaining routes to the beginning of the list |
|---|
| 547 | # and ensures all the controllers have the right mixins. |
|---|
| 548 | # |
|---|
| 549 | # Anyway, if you are calling the URI dispatcher from outside of a Camping server, you'll |
|---|
| 550 | # definitely need to call this at least once to set things up. |
|---|
| 551 | def M |
|---|
| 552 | def M #:nodoc: |
|---|
| 553 | end |
|---|
| 554 | constants.map { |c| |
|---|
| 555 | k=const_get(c) |
|---|
| 556 | k.send :include,C,Base,Helpers,Models |
|---|
| 557 | @r=[k]+r if r-[k]==r |
|---|
| 558 | k.meta_def(:urls){["/#{c.downcase}"]}if !k.respond_to?:urls |
|---|
| 559 | } |
|---|
| 560 | end |
|---|
| 561 | end |
|---|
| 562 | |
|---|
| 563 | |
|---|
| 564 | # Internal controller with no route. Used by #D and C.run to show internal messages. |
|---|
| 565 | class I < R() |
|---|
| 566 | end |
|---|
| 567 | |
|---|
| 568 | self |
|---|
| 569 | end |
|---|
| 570 | |
|---|
| 571 | class << self |
|---|
| 572 | # When you are running many applications, you may want to create independent |
|---|
| 573 | # modules for each Camping application. Namespaces for each. Camping::goes |
|---|
| 574 | # defines a toplevel constant with the whole MVC rack inside. |
|---|
| 575 | # |
|---|
| 576 | # require 'camping' |
|---|
| 577 | # Camping.goes :Blog |
|---|
| 578 | # |
|---|
| 579 | # module Blog::Controllers; ... end |
|---|
| 580 | # module Blog::Models; ... end |
|---|
| 581 | # module Blog::Views; ... end |
|---|
| 582 | # |
|---|
| 583 | def goes(m) |
|---|
| 584 | eval S.gsub(/Camping/,m.to_s), TOPLEVEL_BINDING |
|---|
| 585 | end |
|---|
| 586 | |
|---|
| 587 | # URL escapes a string. |
|---|
| 588 | # |
|---|
| 589 | # Camping.escape("I'd go to the museum straightway!") |
|---|
| 590 | # #=> "I%27d+go+to+the+museum+straightway%21" |
|---|
| 591 | # |
|---|
| 592 | def escape(s); s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.unpack('H2'*$&.size)*'%').upcase}.tr(' ', '+') end |
|---|
| 593 | |
|---|
| 594 | # Unescapes a URL-encoded string. |
|---|
| 595 | # |
|---|
| 596 | # Camping.un("I%27d+go+to+the+museum+straightway%21") |
|---|
| 597 | # #=> "I'd go to the museum straightway!" |
|---|
| 598 | # |
|---|
| 599 | def un(s); s.tr('+', ' ').gsub(/%([\da-f]{2})/in){[$1].pack('H*')} end |
|---|
| 600 | |
|---|
| 601 | # Parses a query string into an Camping::H object. |
|---|
| 602 | # |
|---|
| 603 | # input = Camping.qsp("name=Philarp+Tremain&hair=sandy+blonde") |
|---|
| 604 | # input.name |
|---|
| 605 | # #=> "Philarp Tremaine" |
|---|
| 606 | # |
|---|
| 607 | # Also parses out the Hash-like syntax used in PHP and Rails and builds |
|---|
| 608 | # nested hashes from it. |
|---|
| 609 | # |
|---|
| 610 | # input = Camping.qsp("post[id]=1&post[user]=_why") |
|---|
| 611 | # #=> {'post' => {'id' => '1', 'user' => '_why'}} |
|---|
| 612 | # |
|---|
| 613 | # And finally, Array syntax like: |
|---|
| 614 | # |
|---|
| 615 | # input = Camping.qsp("user[]=_why&user[]=lucky&user[]=stiff") |
|---|
| 616 | # #=> {"user" => ["_why", "lucky", "stiff"]} |
|---|
| 617 | # |
|---|
| 618 | def qsp(q, d='&;', y=nil, z=H[]) |
|---|
| 619 | m = proc {|_,o,n|o.u(n,&m)rescue([*o]<<n)} |
|---|
| 620 | (q.to_s.split(/[#{d}]+ */n) - [""]). |
|---|
| 621 | inject((b,z=z,H[])[0]) { |h,p| |
|---|
| 622 | k, v=un(p).split('=',2) |
|---|
| 623 | h.u(k.split(/[\]\[]+/).reverse. |
|---|
| 624 | inject(y||v) { |x,i| H[i,x] },&m) |
|---|
| 625 | } |
|---|
| 626 | end |
|---|
| 627 | |
|---|
| 628 | # Parses a string of cookies from the <tt>Cookie</tt> header. |
|---|
| 629 | def kp(s); c = qsp(s, ';,'); end |
|---|
| 630 | |
|---|
| 631 | # Fields a request through Camping. For traditional CGI applications, the method can be |
|---|
| 632 | # executed without arguments. |
|---|
| 633 | # |
|---|
| 634 | # if __FILE__ == $0 |
|---|
| 635 | # Camping::Models::Base.establish_connection :adapter => 'sqlite3', |
|---|
| 636 | # :database => 'blog3.db' |
|---|
| 637 | # Camping::Models::Base.logger = Logger.new('camping.log') |
|---|
| 638 | # puts Camping.run |
|---|
| 639 | # end |
|---|
| 640 | # |
|---|
| 641 | # The Camping controller returned from <tt>run</tt> has a <tt>to_s</tt> method in case you |
|---|
| 642 | # are running from CGI or want to output the full HTTP output. In the above example, <tt>puts</tt> |
|---|
| 643 | # will call <tt>to_s</tt> for you. |
|---|
| 644 | # |
|---|
| 645 | # For FastCGI and Webrick-loaded applications, you will need to use a request loop, with <tt>run</tt> |
|---|
| 646 | # at the center, passing in the read +r+ and write +w+ streams. You will also need to mimick or |
|---|
| 647 | # pass in the <tt>ENV</tt> replacement as part of your wrapper. |
|---|
| 648 | # |
|---|
| 649 | # See Camping::FastCGI and Camping::WEBrick for examples. |
|---|
| 650 | # |
|---|
| 651 | def run(r=$stdin,e=ENV) |
|---|
| 652 | X.M |
|---|
| 653 | e = H[e.to_hash] |
|---|
| 654 | k,m,*a=X.D e.PATH_INFO=un("/#{e.PATH_INFO}".gsub(/\/+/,'/')),(e.REQUEST_METHOD||'get').downcase |
|---|
| 655 | k.new(r,e,m).Y.service(*a) |
|---|
| 656 | rescue => x |
|---|
| 657 | X::I.new(r,e,'r500').service(k,m,x) |
|---|
| 658 | end |
|---|
| 659 | |
|---|
| 660 | # The Camping scriptable dispatcher. Any unhandled method call to the app module will |
|---|
| 661 | # be sent to a controller class, specified as an argument. |
|---|
| 662 | # |
|---|
| 663 | # Blog.get(:Index) |
|---|
| 664 | # #=> #<Blog::Controllers::Index ... > |
|---|
| 665 | # |
|---|
| 666 | # The controller object contains all the @cookies, @body, @headers, etc. formulated by |
|---|
| 667 | # the response. |
|---|
| 668 | # |
|---|
| 669 | # You can also feed environment variables and query variables as a hash, the final |
|---|
| 670 | # argument. |
|---|
| 671 | # |
|---|
| 672 | # Blog.post(:Login, :input => {'username' => 'admin', 'password' => 'camping'}) |
|---|
| 673 | # #=> #<Blog::Controllers::Login @user=... > |
|---|
| 674 | # |
|---|
| 675 | # Blog.get(:Info, :env => {'HTTP_HOST' => 'wagon'}) |
|---|
| 676 | # #=> #<Blog::Controllers::Info @env={'HTTP_HOST'=>'wagon'} ...> |
|---|
| 677 | # |
|---|
| 678 | def method_missing(m, c, *a) |
|---|
| 679 | X.M |
|---|
| 680 | k = X.const_get(c).new(StringIO.new, |
|---|
| 681 | H['HTTP_HOST','','SCRIPT_NAME','','HTTP_COOKIE',''],m.to_s) |
|---|
| 682 | H[a.pop].each { |e,f| k.send("#{e}=",f) } if Hash === a[-1] |
|---|
| 683 | k.service(*a) |
|---|
| 684 | end |
|---|
| 685 | end |
|---|
| 686 | |
|---|
| 687 | # Views is an empty module for storing methods which create HTML. The HTML is described |
|---|
| 688 | # using the Markaby language. |
|---|
| 689 | # |
|---|
| 690 | # == Using the layout method |
|---|
| 691 | # |
|---|
| 692 | # If your Views module has a <tt>layout</tt> method defined, it will be called with a block |
|---|
| 693 | # which will insert content from your view. |
|---|
| 694 | module Views; include X, Helpers end |
|---|
| 695 | |
|---|
| 696 | # Models is an empty Ruby module for housing model classes derived |
|---|
| 697 | # from ActiveRecord::Base. As a shortcut, you may derive from Base |
|---|
| 698 | # which is an alias for ActiveRecord::Base. |
|---|
| 699 | # |
|---|
| 700 | # module Camping::Models |
|---|
| 701 | # class Post < Base; belongs_to :user end |
|---|
| 702 | # class User < Base; has_many :posts end |
|---|
| 703 | # end |
|---|
| 704 | # |
|---|
| 705 | # == Where Models are Used |
|---|
| 706 | # |
|---|
| 707 | # Models are used in your controller classes. However, if your model class |
|---|
| 708 | # name conflicts with a controller class name, you will need to refer to it |
|---|
| 709 | # using the Models module. |
|---|
| 710 | # |
|---|
| 711 | # module Camping::Controllers |
|---|
| 712 | # class Post < R '/post/(\d+)' |
|---|
| 713 | # def get(post_id) |
|---|
| 714 | # @post = Models::Post.find post_id |
|---|
| 715 | # render :index |
|---|
| 716 | # end |
|---|
| 717 | # end |
|---|
| 718 | # end |
|---|
| 719 | # |
|---|
| 720 | # Models cannot be referred to in Views at this time. |
|---|
| 721 | module Models |
|---|
| 722 | autoload :Base,'camping/db' |
|---|
| 723 | def Y;self;end |
|---|
| 724 | end |
|---|
| 725 | |
|---|
| 726 | autoload :Mab, 'camping/mab' |
|---|
| 727 | end |
|---|
| 728 | |
|---|