camping 1.1 → 1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +69 -0
- data/examples/blog/blog.rb +45 -24
- data/examples/charts/charts.rb +89 -0
- data/examples/charts/pie.rb +70 -0
- data/examples/charts/start +6 -0
- data/examples/serve +57 -0
- data/examples/tepee/start +6 -0
- data/examples/tepee/tepee.rb +137 -0
- data/lib/camping-unabridged.rb +468 -0
- data/lib/camping.rb +49 -105
- metadata +31 -12
- data/lib/camping-mural.rb +0 -43
@@ -0,0 +1,137 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
$:.unshift File.dirname(__FILE__) + "/../../lib"
|
3
|
+
%w(rubygems redcloth camping acts_as_versioned).each { |lib| require lib }
|
4
|
+
|
5
|
+
Camping.goes :Tepee
|
6
|
+
|
7
|
+
module Tepee::Models
|
8
|
+
def self.schema(&block)
|
9
|
+
@@schema = block if block_given?
|
10
|
+
@@schema
|
11
|
+
end
|
12
|
+
|
13
|
+
class Page < Base
|
14
|
+
PAGE_LINK = /\[\[([^\]|]*)[|]?([^\]]*)\]\]/
|
15
|
+
validates_uniqueness_of :title
|
16
|
+
before_save { |r| r.title = r.title.underscore }
|
17
|
+
acts_as_versioned
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Tepee::Models.schema do
|
22
|
+
create_table :pages, :force => true do |t|
|
23
|
+
t.column :title, :string, :limit => 255
|
24
|
+
t.column :body, :text
|
25
|
+
end
|
26
|
+
Tepee::Models::Page.create_versioned_table
|
27
|
+
end
|
28
|
+
|
29
|
+
module Tepee::Controllers
|
30
|
+
class Index < R '/'
|
31
|
+
def get
|
32
|
+
redirect Show, 'home_page'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class List < R '/list'
|
37
|
+
def get
|
38
|
+
@pages = Page.find :all, :order => 'title'
|
39
|
+
render :list
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Show < R '/s/(\w+)', '/s/(\w+)/(\d+)'
|
44
|
+
def get page_name, version = nil
|
45
|
+
redirect(Edit, page_name, 1) and return unless @page = Page.find_by_title(page_name)
|
46
|
+
@version = (version.nil? or version == @page.version.to_s) ? @page : @page.versions.find_by_version(version)
|
47
|
+
render :show
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Edit < R '/e/(\w+)/(\d+)', '/e/(\w+)'
|
52
|
+
def get page_name, version = nil
|
53
|
+
@page = Page.find_or_create_by_title(page_name)
|
54
|
+
@page = @page.versions.find_by_version(version) unless version.nil? or version == @page.version.to_s
|
55
|
+
render :edit
|
56
|
+
end
|
57
|
+
|
58
|
+
def post page_name
|
59
|
+
Page.find_or_create_by_title(page_name).update_attributes :body => input.post_body and redirect Show, page_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Tepee::Views
|
65
|
+
def layout
|
66
|
+
html do
|
67
|
+
head do
|
68
|
+
title 'test'
|
69
|
+
end
|
70
|
+
body do
|
71
|
+
p do
|
72
|
+
small do
|
73
|
+
span "welcome to " ; a 'tepee', :href => "http://code.whytheluckystiff.net/svn/camping/trunk/examples/tepee/"
|
74
|
+
span '. go ' ; a 'home', :href => R(Show, 'home_page')
|
75
|
+
span '. list all ' ; a 'pages', :href => R(List)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
div.content do
|
79
|
+
self << yield
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def show
|
86
|
+
h1 @page.title
|
87
|
+
div { _markup @version.body }
|
88
|
+
p do
|
89
|
+
a 'edit', :href => R(Edit, @version.title, @version.version)
|
90
|
+
a 'back', :href => R(Show, @version.title, @version.version-1) unless @version.version == 1
|
91
|
+
a 'next', :href => R(Show, @version.title, @version.version+1) unless @version.version == @page.version
|
92
|
+
a 'current', :href => R(Show, @version.title) unless @version.version == @page.version
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def edit
|
97
|
+
form :method => 'post', :action => R(Edit, @page.title) do
|
98
|
+
p do
|
99
|
+
label 'Body' ; br
|
100
|
+
textarea @page.body, :name => 'post_body', :rows => 50, :cols => 100
|
101
|
+
end
|
102
|
+
|
103
|
+
p do
|
104
|
+
input :type => 'submit'
|
105
|
+
a 'cancel', :href => R(Show, @page.title, @page.version)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def list
|
111
|
+
h1 'all pages'
|
112
|
+
ul { @pages.each { |p| li { a p.title, :href => R(Show, p.title) } } }
|
113
|
+
end
|
114
|
+
|
115
|
+
def _markup body
|
116
|
+
return '' if body.blank?
|
117
|
+
body.gsub!(Tepee::Models::Page::PAGE_LINK) do
|
118
|
+
page = title = $1.underscore
|
119
|
+
title = $2 unless $2.empty?
|
120
|
+
if Tepee::Models::Page.find(:all, :select => 'title').collect { |p| p.title }.include?(page)
|
121
|
+
%Q{<a href="#{R Show, page}">#{title}</a>}
|
122
|
+
else
|
123
|
+
%Q{<span>#{title}<a href="#{R Edit, page, 1}">?</a></span>}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
RedCloth.new(body, [ :hard_breaks ]).to_html
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
db_exists = File.exists?('tepee.db')
|
131
|
+
Tepee::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'tepee.db'
|
132
|
+
Tepee::Models::Base.logger = Logger.new('camping.log')
|
133
|
+
ActiveRecord::Schema.define(&Tepee::Models.schema) unless db_exists
|
134
|
+
|
135
|
+
if __FILE__ == $0
|
136
|
+
Tepee.run
|
137
|
+
end
|
@@ -0,0 +1,468 @@
|
|
1
|
+
%w[rubygems active_record markaby metaid ostruct tempfile].each { |lib| require lib }
|
2
|
+
|
3
|
+
# == Camping
|
4
|
+
#
|
5
|
+
# The camping module contains three modules for separating your application:
|
6
|
+
#
|
7
|
+
# * Camping::Models for storing classes derived from ActiveRecord::Base.
|
8
|
+
# * Camping::Controllers for storing controller classes, which map URLs to code.
|
9
|
+
# * Camping::Views for storing methods which generate HTML.
|
10
|
+
#
|
11
|
+
# Of use to you is also one module for storing helpful additional methods:
|
12
|
+
#
|
13
|
+
# * Camping::Helpers which can be used in controllers and views.
|
14
|
+
#
|
15
|
+
# == The postamble
|
16
|
+
#
|
17
|
+
# Most Camping applications contain the entire application in a single script.
|
18
|
+
# The script begins by requiring Camping, then fills each of the three modules
|
19
|
+
# described above with classes and methods. Finally, a postamble puts the wheels
|
20
|
+
# in motion.
|
21
|
+
#
|
22
|
+
# if __FILE__ == $0
|
23
|
+
# Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
|
24
|
+
# Camping::Models::Base.logger = Logger.new('camping.log')
|
25
|
+
# Camping.run
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# In the postamble, your job is to setup Camping::Models::Base (see: ActiveRecord::Base)
|
29
|
+
# and call Camping::run in a request loop. The above postamble is for a standard
|
30
|
+
# CGI setup, where the web server manages the request loop and calls the script once
|
31
|
+
# for every request.
|
32
|
+
#
|
33
|
+
# For other configurations, see
|
34
|
+
# http://code.whytheluckystiff.net/camping/wiki/PostAmbles
|
35
|
+
module Camping
|
36
|
+
C = self
|
37
|
+
S = File.read(__FILE__).gsub(/_{2}FILE_{2}/,__FILE__.dump)
|
38
|
+
|
39
|
+
# Helpers contains methods available in your controllers and views.
|
40
|
+
module Helpers
|
41
|
+
# From inside your controllers and views, you will often need to figure out
|
42
|
+
# the route used to get to a certain controller +c+. Pass the controller class
|
43
|
+
# and any arguments into the R method, a string containing the route will be
|
44
|
+
# returned to you.
|
45
|
+
#
|
46
|
+
# Assuming you have a specific route in an edit controller:
|
47
|
+
#
|
48
|
+
# class Edit < R '/edit/(\d+)'
|
49
|
+
#
|
50
|
+
# A specific route to the Edit controller can be built with:
|
51
|
+
#
|
52
|
+
# R(Edit, 1)
|
53
|
+
#
|
54
|
+
# Which outputs: <tt>/edit/1</tt>.
|
55
|
+
#
|
56
|
+
# You may also pass in a model object and the ID of the object will be used.
|
57
|
+
#
|
58
|
+
# If a controller has many routes, the route will be selected if it is the
|
59
|
+
# first in the routing list to have the right number of arguments.
|
60
|
+
#
|
61
|
+
# Keep in mind that this route doesn't include the root path. Occassionally
|
62
|
+
# you will need to use <tt>/</tt> (the slash method above).
|
63
|
+
def R(c,*args)
|
64
|
+
p = /\(.+?\)/
|
65
|
+
args.inject(c.urls.detect{|x|x.scan(p).size==args.size}.dup){|str,a|
|
66
|
+
str.sub(p,(a.method(a.class.primary_key)[] rescue a).to_s)
|
67
|
+
}
|
68
|
+
end
|
69
|
+
# Shows AR validation errors for the object passed.
|
70
|
+
# There is no output if there are no errors.
|
71
|
+
#
|
72
|
+
# An example might look like:
|
73
|
+
#
|
74
|
+
# errors_for @post
|
75
|
+
#
|
76
|
+
# Might (depending on actual data) render something like this in Markaby:
|
77
|
+
#
|
78
|
+
# ul.errors do
|
79
|
+
# li "Body can't be empty"
|
80
|
+
# li "Title must be unique"
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# Add a simple ul.errors {color:red; font-weight:bold;} CSS rule and you
|
84
|
+
# have built-in, usable error checking in only one line of code. :-)
|
85
|
+
#
|
86
|
+
# See AR validation documentation for details on validations.
|
87
|
+
def errors_for(o); ul.errors { o.errors.each_full { |er| li er } } unless o.errors.empty?; end
|
88
|
+
# Simply builds the complete URL from a relative or absolute path +p+. If your
|
89
|
+
# application is running from <tt>/blog</tt>:
|
90
|
+
#
|
91
|
+
# self / "/view/1" #=> "/blog/view/1"
|
92
|
+
# self / "styles.css" #=> "styles.css"
|
93
|
+
# self / R(Edit, 1) #=> "/blog/edit/1"
|
94
|
+
#
|
95
|
+
def /(p); p[/^\//]?@root+p:p end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Controllers is a module for placing classes which handle URLs. This is done
|
99
|
+
# by defining a route to each class using the Controllers::R method.
|
100
|
+
#
|
101
|
+
# module Camping::Controllers
|
102
|
+
# class Edit < R '/edit/(\d+)'
|
103
|
+
# def get; end
|
104
|
+
# def post; end
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# If no route is set, Camping will guess the route from the class name.
|
109
|
+
# The rule is very simple: the route becomes a slash followed by the lowercased
|
110
|
+
# class name. See Controllers::D for the complete rules of dispatch.
|
111
|
+
#
|
112
|
+
# == Special classes
|
113
|
+
#
|
114
|
+
# There are two special classes used for handling 404 and 500 errors. The
|
115
|
+
# NotFound class handles URLs not found. The ServerError class handles exceptions
|
116
|
+
# uncaught by your application.
|
117
|
+
module Controllers
|
118
|
+
# Controllers::Base is built into each controller by way of the generic routing
|
119
|
+
# class Controllers::R. In some ways, this class is trying to do too much, but
|
120
|
+
# it saves code for all the glue to stay in one place.
|
121
|
+
#
|
122
|
+
# Forgivable, considering that it's only really a handful of methods and accessors.
|
123
|
+
#
|
124
|
+
# == Treating controller methods like Response objects
|
125
|
+
#
|
126
|
+
# Camping originally came with a barebones Response object, but it's often much more readable
|
127
|
+
# to just use your controller as the response.
|
128
|
+
#
|
129
|
+
# Go ahead and alter the status, cookies, headers and body instance variables as you
|
130
|
+
# see fit in order to customize the response.
|
131
|
+
#
|
132
|
+
# module Camping::Controllers
|
133
|
+
# class SoftLink
|
134
|
+
# def get
|
135
|
+
# redirect "/"
|
136
|
+
# end
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# Is equivalent to:
|
141
|
+
#
|
142
|
+
# module Camping::Controllers
|
143
|
+
# class SoftLink
|
144
|
+
# def get
|
145
|
+
# @status = 302
|
146
|
+
# @headers['Location'] = "/"
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
module Base
|
152
|
+
include Helpers
|
153
|
+
attr_accessor :input, :cookies, :headers, :body, :status, :root
|
154
|
+
# Display a view, calling it by its method name +m+. If a <tt>layout</tt>
|
155
|
+
# method is found in Camping::Views, it will be used to wrap the HTML.
|
156
|
+
#
|
157
|
+
# module Camping::Controllers
|
158
|
+
# class Show
|
159
|
+
# def get
|
160
|
+
# @posts = Post.find :all
|
161
|
+
# render :index
|
162
|
+
# end
|
163
|
+
# end
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
def render(m); end; undef_method :render
|
167
|
+
|
168
|
+
# Any stray method calls will be passed to Markaby. This means you can reply
|
169
|
+
# with HTML directly from your controller for quick debugging.
|
170
|
+
#
|
171
|
+
# module Camping::Controllers
|
172
|
+
# class Info
|
173
|
+
# def get; code ENV.inspect end
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# If you have a <tt>layout</tt> method in Camping::Views, it will be used to
|
178
|
+
# wrap the HTML.
|
179
|
+
def method_missing(m, *args, &blk)
|
180
|
+
str = m==:render ? markaview(*args, &blk):eval("markaby.#{m}(*args, &blk)")
|
181
|
+
str = markaview(:layout) { str } rescue nil
|
182
|
+
r(200, str.to_s)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Formulate a redirect response: a 302 status with <tt>Location</tt> header
|
186
|
+
# and a blank body. If +c+ is a string, the root path will be added. If
|
187
|
+
# +c+ is a controller class, Helpers::R will be used to route the redirect
|
188
|
+
# and the root path will be added.
|
189
|
+
#
|
190
|
+
# So, given a root of <tt>/articles</tt>:
|
191
|
+
#
|
192
|
+
# redirect "view/12" # redirects to "/articles/view/12"
|
193
|
+
# redirect View, 12 # redirects to "/articles/view/12"
|
194
|
+
#
|
195
|
+
def redirect(c, *args)
|
196
|
+
c = R(c,*args) if c.respond_to? :urls
|
197
|
+
r(302, '', 'Location' => self/c)
|
198
|
+
end
|
199
|
+
|
200
|
+
# A quick means of setting this controller's status, body and headers.
|
201
|
+
# Used internally by Camping, but... by all means...
|
202
|
+
#
|
203
|
+
# r(302, '', 'Location' => self / "/view/12")
|
204
|
+
#
|
205
|
+
# Is equivalent to:
|
206
|
+
#
|
207
|
+
# redirect "/view/12"
|
208
|
+
#
|
209
|
+
def r(s, b, h = {}); @status = s; @headers.merge!(h); @body = b; end
|
210
|
+
|
211
|
+
def service(r, e, m, a) #:nodoc:
|
212
|
+
@status, @headers, @root = 200, {}, e['SCRIPT_NAME']
|
213
|
+
cook = C.cookie_parse(e['HTTP_COOKIE'] || e['COOKIE'])
|
214
|
+
qs = C.qs_parse(e['QUERY_STRING'])
|
215
|
+
if "POST" == m
|
216
|
+
inp = r.read(e['CONTENT_LENGTH'].to_i)
|
217
|
+
if %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)|n.match(e['CONTENT_TYPE'])
|
218
|
+
b = "--#$1"
|
219
|
+
inp.split(/(?:\r?\n|\A)#{ Regexp::quote( b ) }(?:--)?\r\n/m).each { |pt|
|
220
|
+
h,v=pt.split("\r\n\r\n",2);fh={}
|
221
|
+
[:name, :filename].each { |x|
|
222
|
+
fh[x] = $1 if h =~ /^Content-Disposition: form-data;.*(?:\s#{x}="([^"]+)")/m
|
223
|
+
}
|
224
|
+
fn = fh[:name]
|
225
|
+
if fh[:filename]
|
226
|
+
fh[:type]=$1 if h =~ /^Content-Type: (.+?)(\r\n|\Z)/m
|
227
|
+
fh[:tempfile]=Tempfile.new("#{C}").instance_eval {binmode;write v;rewind;self}
|
228
|
+
else
|
229
|
+
fh=v
|
230
|
+
end
|
231
|
+
qs[fn]=fh if fn
|
232
|
+
}
|
233
|
+
else
|
234
|
+
qs.merge!(C.qs_parse(inp))
|
235
|
+
end
|
236
|
+
end
|
237
|
+
@cookies, @input = [cook, qs].map{|_|OpenStruct.new(_)}
|
238
|
+
|
239
|
+
@body = method( m.downcase ).call(*a)
|
240
|
+
@headers['Set-Cookie'] = @cookies.marshal_dump.map { |k,v| "#{k}=#{C.escape(v)}; path=/" if v != cook[k] }.compact
|
241
|
+
self
|
242
|
+
end
|
243
|
+
def to_s #:nodoc:
|
244
|
+
"Status: #{@status}\n#{{'Content-Type'=>'text/html'}.merge(@headers).map{|k,v|v.to_a.map{|v2|"#{k}: #{v2}"}}.flatten.join("\n")}\n\n#{@body}"
|
245
|
+
end
|
246
|
+
private
|
247
|
+
def markaby
|
248
|
+
Mab.new( instance_variables.map { |iv|
|
249
|
+
[iv[1..-1], instance_variable_get(iv)] }, {} )
|
250
|
+
end
|
251
|
+
def markaview(m, *args, &blk)
|
252
|
+
b=markaby
|
253
|
+
b.method(m).call(*args, &blk)
|
254
|
+
b.to_s
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# The R class is the parent class for all controllers and ensures they all get the Base mixin.
|
259
|
+
class R; include Base end
|
260
|
+
|
261
|
+
# The NotFound class is a special controller class for handling 404 errors, in case you'd
|
262
|
+
# like to alter the appearance of the 404. The path is passed in as +p+.
|
263
|
+
#
|
264
|
+
# module Camping::Controllers
|
265
|
+
# class NotFound
|
266
|
+
# def get(p)
|
267
|
+
# @status = 404
|
268
|
+
# div do
|
269
|
+
# h1 'Camping Problem!'
|
270
|
+
# h2 "#{p} not found"
|
271
|
+
# end
|
272
|
+
# end
|
273
|
+
# end
|
274
|
+
# end
|
275
|
+
#
|
276
|
+
class NotFound; def get(p); r(404, div{h1("#{C} Problem!")+h2("#{p} not found")}); end end
|
277
|
+
|
278
|
+
# The ServerError class is a special controller class for handling many (but not all) 500 errors.
|
279
|
+
# If there is a parse error in Camping or in your application's source code, it will not be caught
|
280
|
+
# by Camping. The controller class +k+ and request method +m+ (GET, POST, etc.) where the error
|
281
|
+
# took place are passed in, along with the Exception +e+ which can be mined for useful info.
|
282
|
+
#
|
283
|
+
# module Camping::Controllers
|
284
|
+
# class ServerError
|
285
|
+
# def get(k,m,e)
|
286
|
+
# @status = 500
|
287
|
+
# div do
|
288
|
+
# h1 'Camping Problem!'
|
289
|
+
# h2 "in #{k}.#{m}"
|
290
|
+
# h3 "#{e.class} #{e.message}:"
|
291
|
+
# ul do
|
292
|
+
# e.backtrace.each do |bt|
|
293
|
+
# li bt
|
294
|
+
# end
|
295
|
+
# end
|
296
|
+
# end
|
297
|
+
# end
|
298
|
+
# end
|
299
|
+
# end
|
300
|
+
#
|
301
|
+
class ServerError; include Base; def get(k,m,e); r(500, markaby.div{ h1 "#{C} Problem!"; h2 "#{k}.#{m}"; h3 "#{e.class} #{e.message}:"; ul { e.backtrace.each { |bt| li bt } } }) end end
|
302
|
+
|
303
|
+
class << self
|
304
|
+
# Add routes to a controller class by piling them into the R method.
|
305
|
+
#
|
306
|
+
# module Camping::Controllers
|
307
|
+
# class Edit < R '/edit/(\d+)', '/new'
|
308
|
+
# def get(id)
|
309
|
+
# if id # edit
|
310
|
+
# else # new
|
311
|
+
# end
|
312
|
+
# end
|
313
|
+
# end
|
314
|
+
# end
|
315
|
+
#
|
316
|
+
# You will need to use routes in either of these cases:
|
317
|
+
#
|
318
|
+
# * You want to assign multiple routes to a controller.
|
319
|
+
# * You want your controller to receive arguments.
|
320
|
+
#
|
321
|
+
# Most of the time the rules inferred by dispatch method Controllers::D will get you
|
322
|
+
# by just fine.
|
323
|
+
def R(*urls); Class.new(R) { meta_def(:inherited) { |c| c.meta_def(:urls) { urls } } }; end
|
324
|
+
|
325
|
+
# Dispatch routes to controller classes. Classes are searched in no particular order.
|
326
|
+
# For each class, routes are checked for a match based on their order in the routing list
|
327
|
+
# given to Controllers::R. If no routes were given, the dispatcher uses a slash followed
|
328
|
+
# by the name of the controller lowercased.
|
329
|
+
def D(path)
|
330
|
+
constants.inject(nil) do |d,c|
|
331
|
+
k = const_get(c)
|
332
|
+
k.meta_def(:urls){["/#{c.downcase}"]}if !(k<R)
|
333
|
+
d||([k, $~[1..-1]] if k.urls.find { |x| path =~ /^#{x}\/?$/ })
|
334
|
+
end||[NotFound, [path]]
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class << self
|
340
|
+
# When you are running many applications, you may want to create independent
|
341
|
+
# modules for each Camping application. Namespaces for each. Camping::goes
|
342
|
+
# defines a toplevel constant with the whole MVC rack inside.
|
343
|
+
#
|
344
|
+
# require 'camping'
|
345
|
+
# Camping.goes :Blog
|
346
|
+
#
|
347
|
+
# module Blog::Controllers; ... end
|
348
|
+
# module Blog::Models; ... end
|
349
|
+
# module Blog::Views; ... end
|
350
|
+
#
|
351
|
+
def goes(m)
|
352
|
+
eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING)
|
353
|
+
end
|
354
|
+
|
355
|
+
# URL escapes a string.
|
356
|
+
#
|
357
|
+
# Camping.escape("I'd go to the museum straightway!")
|
358
|
+
# #=> "I%27d+go+to+the+museum+straightway%21"
|
359
|
+
#
|
360
|
+
|
361
|
+
def escape(s); s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr(' ', '+') end
|
362
|
+
# Unescapes a URL-encoded string.
|
363
|
+
#
|
364
|
+
# Camping.unescape("I%27d+go+to+the+museum+straightway%21")
|
365
|
+
# #=> "I'd go to the museum straightway!"
|
366
|
+
#
|
367
|
+
def unescape(s); s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')} end
|
368
|
+
|
369
|
+
# Parses a query string into an OpenStruct object.
|
370
|
+
#
|
371
|
+
# input = Camping.qs_parse("name=Philarp+Tremain&hair=sandy+blonde")
|
372
|
+
# input.name
|
373
|
+
# #=> "Philarp Tremaine"
|
374
|
+
#
|
375
|
+
def qs_parse(qs, d = '&;'); (qs||'').split(/[#{d}] */n).
|
376
|
+
inject({}){|hsh, p|k, v = p.split('=',2).map {|v| unescape(v)}; hsh[k] = v unless v.blank?; hsh} end
|
377
|
+
|
378
|
+
# Parses a string of cookies from the <tt>Cookie</tt> header.
|
379
|
+
def cookie_parse(s); c = qs_parse(s, ';,'); end
|
380
|
+
|
381
|
+
# Fields a request through Camping. For traditional CGI applications, the method can be
|
382
|
+
# executed without arguments.
|
383
|
+
#
|
384
|
+
# if __FILE__ == $0
|
385
|
+
# Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
|
386
|
+
# Camping::Models::Base.logger = Logger.new('camping.log')
|
387
|
+
# Camping.run
|
388
|
+
# end
|
389
|
+
#
|
390
|
+
# For FastCGI and Webrick-loaded applications, you will need to use a request loop, with <tt>run</tt>
|
391
|
+
# at the center, passing in the read +r+ and write +w+ streams. You will also need to mimick or
|
392
|
+
# replace <tt>ENV</tt> as part of your wrapper.
|
393
|
+
#
|
394
|
+
# if __FILE__ == $0
|
395
|
+
# require 'fcgi'
|
396
|
+
# Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
|
397
|
+
# Camping::Models::Base.logger = Logger.new('camping.log')
|
398
|
+
# FCGI.each do |req|
|
399
|
+
# ENV.replace req.env
|
400
|
+
# Camping.run req.in, req.out
|
401
|
+
# req.finish
|
402
|
+
# end
|
403
|
+
# end
|
404
|
+
# end
|
405
|
+
#
|
406
|
+
def run(r=$stdin,w=$stdout)
|
407
|
+
w <<
|
408
|
+
begin
|
409
|
+
k, a = Controllers.D "/#{ENV['PATH_INFO']}".gsub(%r!/+!,'/')
|
410
|
+
m = ENV['REQUEST_METHOD']||"GET"
|
411
|
+
k.class_eval { include C; include Controllers::Base; include Models }
|
412
|
+
o = k.new
|
413
|
+
o.service(r, ENV, m, a)
|
414
|
+
rescue => e
|
415
|
+
Controllers::ServerError.new.service(r, ENV, "GET", [k,m,e])
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# Models is an empty Ruby module for housing model classes derived
|
421
|
+
# from ActiveRecord::Base. As a shortcut, you may derive from Base
|
422
|
+
# which is an alias for ActiveRecord::Base.
|
423
|
+
#
|
424
|
+
# module Camping::Models
|
425
|
+
# class Post < Base; belongs_to :user end
|
426
|
+
# class User < Base; has_many :posts end
|
427
|
+
# end
|
428
|
+
#
|
429
|
+
# == Where Models are Used
|
430
|
+
#
|
431
|
+
# Models are used in your controller classes. However, if your model class
|
432
|
+
# name conflicts with a controller class name, you will need to refer to it
|
433
|
+
# using the Models module.
|
434
|
+
#
|
435
|
+
# module Camping::Controllers
|
436
|
+
# class Post < R '/post/(\d+)'
|
437
|
+
# def get(post_id)
|
438
|
+
# @post = Models::Post.find post_id
|
439
|
+
# render :index
|
440
|
+
# end
|
441
|
+
# end
|
442
|
+
# end
|
443
|
+
#
|
444
|
+
# Models cannot be referred to in Views at this time.
|
445
|
+
module Models; end
|
446
|
+
|
447
|
+
# Views is an empty module for storing methods which create HTML. The HTML is described
|
448
|
+
# using the Markaby language.
|
449
|
+
#
|
450
|
+
# == Using the layout method
|
451
|
+
#
|
452
|
+
# If your Views module has a <tt>layout</tt> method defined, it will be called with a block
|
453
|
+
# which will insert content from your view.
|
454
|
+
module Views; include Controllers; include Helpers end
|
455
|
+
Models::Base = ActiveRecord::Base
|
456
|
+
|
457
|
+
# The Mab class wraps Markaby, allowing it to run methods from Camping::Views
|
458
|
+
# and also to replace :href and :action attributes in tags by prefixing the root
|
459
|
+
# path.
|
460
|
+
class Mab < Markaby::Builder
|
461
|
+
include Views
|
462
|
+
def tag!(*g,&b)
|
463
|
+
h=g[-1]
|
464
|
+
[:href,:action].each{|a|(h[a]=self/h[a])rescue 0}
|
465
|
+
super
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|