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
data/README
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
== Camping, a Microframework
|
2
|
+
|
3
|
+
Camping is a web framework which consistently stays at less than 4kb of code.
|
4
|
+
You can probably view the complete source code on a single page. But, you know,
|
5
|
+
it's so small that, if you think about it, what can it really do?
|
6
|
+
|
7
|
+
The idea here is to store a complete fledgling web application in a single file
|
8
|
+
like many small CGIs. But to organize it as a Model-View-Controller application
|
9
|
+
like Rails does. You can then easily move it to Rails once you've got it going.
|
10
|
+
|
11
|
+
A skeleton might be:
|
12
|
+
|
13
|
+
require 'camping'
|
14
|
+
|
15
|
+
module Camping::Models
|
16
|
+
class Post < Base; belongs_to :user; end
|
17
|
+
class Comment < Base; belongs_to :user; end
|
18
|
+
class User < Base; end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Camping::Controllers
|
22
|
+
class Index < R '/'
|
23
|
+
def get
|
24
|
+
@posts = Post.find :all
|
25
|
+
render :index
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Camping::Views
|
31
|
+
def layout
|
32
|
+
html do
|
33
|
+
body do
|
34
|
+
self << yield
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def index
|
40
|
+
for post in @posts
|
41
|
+
h1 post.title
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
if __FILE__ == $0
|
47
|
+
Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
|
48
|
+
Camping::Models::Base.logger = Logger.new('camping.log')
|
49
|
+
Camping.run
|
50
|
+
end
|
51
|
+
|
52
|
+
Some things you might have noticed:
|
53
|
+
|
54
|
+
* Camping::Models uses ActiveRecord to do its work. We love ActiveRecord!
|
55
|
+
* Camping::Controllers can be assigned URLs in the class definition. Neat?
|
56
|
+
* Camping::Views describes HTML using pure Ruby. Markup as Ruby, which we
|
57
|
+
call Markaby.
|
58
|
+
|
59
|
+
If you want to write larger applications with Camping, you are encouraged to
|
60
|
+
split the application into distinct parts which can be mounted at URLs on your
|
61
|
+
web server. You might have a blog at /blog and a wiki at /wiki. Each
|
62
|
+
self-contained. But you can certainly share layouts and models by storing them
|
63
|
+
in plain Ruby scripts.
|
64
|
+
|
65
|
+
Interested yet? Okay, okay, one step at a time.
|
66
|
+
|
67
|
+
== Installation
|
68
|
+
|
69
|
+
* <tt>gem install camping</tt>
|
data/examples/blog/blog.rb
CHANGED
@@ -1,16 +1,30 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
$:.unshift File.dirname(__FILE__) + "/../../lib"
|
4
4
|
require 'rubygems'
|
5
5
|
require 'camping'
|
6
6
|
|
7
|
-
|
7
|
+
Camping.goes :Blog
|
8
|
+
|
9
|
+
module Blog::Models
|
10
|
+
def self.schema(&block)
|
11
|
+
@@schema = block if block_given?
|
12
|
+
@@schema
|
13
|
+
end
|
14
|
+
|
8
15
|
class Post < Base; belongs_to :user; end
|
9
16
|
class Comment < Base; belongs_to :user; end
|
10
17
|
class User < Base; end
|
11
18
|
end
|
12
19
|
|
13
|
-
|
20
|
+
Blog::Models.schema do
|
21
|
+
# create_table :posts, :force => true do |t|
|
22
|
+
# t.column :title, :string, :limit => 255
|
23
|
+
# t.column :body, :text
|
24
|
+
# end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Blog::Controllers
|
14
28
|
class Index < R '/'
|
15
29
|
def get
|
16
30
|
@posts = Post.find :all
|
@@ -32,16 +46,20 @@ module Camping::Controllers
|
|
32
46
|
end
|
33
47
|
end
|
34
48
|
|
35
|
-
class Info
|
36
|
-
def get
|
37
|
-
|
49
|
+
class Info < R '/info/(\d+)', '/info/(\w+)/(\d+)', '/info', '/info/(\d+)/(\d+)/(\d+)/([\w-]+)'
|
50
|
+
def get(*args)
|
51
|
+
div do
|
52
|
+
code args.inspect; br; br
|
53
|
+
code ENV.inspect; br
|
54
|
+
code "Link: #{R(Info, 1, 2)}"
|
55
|
+
end
|
38
56
|
end
|
39
57
|
end
|
40
58
|
|
41
59
|
class View < R '/view/(\d+)'
|
42
60
|
def get post_id
|
43
61
|
@post = Post.find post_id
|
44
|
-
@comments = Comment.find :all, :conditions => ['post_id = ?', post_id]
|
62
|
+
@comments = Models::Comment.find :all, :conditions => ['post_id = ?', post_id]
|
45
63
|
render :view
|
46
64
|
end
|
47
65
|
end
|
@@ -64,7 +82,7 @@ module Camping::Controllers
|
|
64
82
|
|
65
83
|
class Comment
|
66
84
|
def post
|
67
|
-
Comment.create(:username => input.post_username,
|
85
|
+
Models::Comment.create(:username => input.post_username,
|
68
86
|
:body => input.post_body, :post_id => input.post_id)
|
69
87
|
redirect View, input.post_id
|
70
88
|
end
|
@@ -99,17 +117,17 @@ module Camping::Controllers
|
|
99
117
|
end
|
100
118
|
end
|
101
119
|
|
102
|
-
module
|
120
|
+
module Blog::Views
|
103
121
|
|
104
122
|
def layout
|
105
123
|
html do
|
106
124
|
head do
|
107
125
|
title 'blog'
|
108
126
|
link :rel => 'stylesheet', :type => 'text/css',
|
109
|
-
:href => 'styles.css', :media => 'screen'
|
127
|
+
:href => '/styles.css', :media => 'screen'
|
110
128
|
end
|
111
129
|
body do
|
112
|
-
h1.header { a 'blog', :href =>
|
130
|
+
h1.header { a 'blog', :href => R(Index) }
|
113
131
|
div.content do
|
114
132
|
self << yield
|
115
133
|
end
|
@@ -120,7 +138,7 @@ module Camping::Views
|
|
120
138
|
def index
|
121
139
|
if @posts.empty?
|
122
140
|
p 'No posts found.'
|
123
|
-
p { a 'Add', :href =>
|
141
|
+
p { a 'Add', :href => R(Add) }
|
124
142
|
else
|
125
143
|
for post in @posts
|
126
144
|
_post(post)
|
@@ -130,17 +148,17 @@ module Camping::Views
|
|
130
148
|
|
131
149
|
def login
|
132
150
|
p { b @login }
|
133
|
-
p { a 'Continue', :href =>
|
151
|
+
p { a 'Continue', :href => R(Add) }
|
134
152
|
end
|
135
153
|
|
136
154
|
def logout
|
137
155
|
p "You have been logged out."
|
138
|
-
p { a 'Continue', :href =>
|
156
|
+
p { a 'Continue', :href => R(Index) }
|
139
157
|
end
|
140
158
|
|
141
159
|
def add
|
142
160
|
if @session
|
143
|
-
_form(post, :action =>
|
161
|
+
_form(post, :action => R(Add))
|
144
162
|
else
|
145
163
|
_login
|
146
164
|
end
|
@@ -148,7 +166,7 @@ module Camping::Views
|
|
148
166
|
|
149
167
|
def edit
|
150
168
|
if @session
|
151
|
-
_form(post, :action =>
|
169
|
+
_form(post, :action => R(Edit))
|
152
170
|
else
|
153
171
|
_login
|
154
172
|
end
|
@@ -163,7 +181,7 @@ module Camping::Views
|
|
163
181
|
p c.body
|
164
182
|
end
|
165
183
|
|
166
|
-
form :action =>
|
184
|
+
form :action => R(Comment), :method => 'post' do
|
167
185
|
label 'Name', :for => 'post_username'; br
|
168
186
|
input :name => 'post_username', :type => 'text'; br
|
169
187
|
label 'Comment', :for => 'post_body'; br
|
@@ -175,7 +193,7 @@ module Camping::Views
|
|
175
193
|
|
176
194
|
# partials
|
177
195
|
def _login
|
178
|
-
form :action =>
|
196
|
+
form :action => R(Login), :method => 'post' do
|
179
197
|
label 'Username', :for => 'username'; br
|
180
198
|
input :name => 'username', :type => 'text'; br
|
181
199
|
|
@@ -190,15 +208,15 @@ module Camping::Views
|
|
190
208
|
h1 post.title
|
191
209
|
p post.body
|
192
210
|
p do
|
193
|
-
a "Edit", :href =>
|
194
|
-
a "View", :href =>
|
211
|
+
a "Edit", :href => R(Edit, post)
|
212
|
+
a "View", :href => R(View, post)
|
195
213
|
end
|
196
214
|
end
|
197
215
|
|
198
216
|
def _form(post, opts)
|
199
217
|
p do
|
200
218
|
text "You are logged in as #{@session.username} | "
|
201
|
-
a 'Logout', :href =>
|
219
|
+
a 'Logout', :href => R(Logout)
|
202
220
|
end
|
203
221
|
form({:method => 'post'}.merge(opts)) do
|
204
222
|
label 'Title', :for => 'post_title'; br
|
@@ -214,8 +232,11 @@ module Camping::Views
|
|
214
232
|
end
|
215
233
|
end
|
216
234
|
|
235
|
+
db_exists = File.exists?('blog3.db')
|
236
|
+
Blog::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
|
237
|
+
Blog::Models::Base.logger = Logger.new('camping.log')
|
238
|
+
ActiveRecord::Schema.define(&Blog::Models.schema) unless db_exists
|
239
|
+
|
217
240
|
if __FILE__ == $0
|
218
|
-
|
219
|
-
Camping::Models::Base.logger = Logger.new('camping.log')
|
220
|
-
Camping.run
|
241
|
+
Blog.run
|
221
242
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__) + "/../../lib"
|
4
|
+
$:.unshift File.dirname(__FILE__)
|
5
|
+
require 'rubygems'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'camping'
|
8
|
+
require 'rvg/rvg'
|
9
|
+
require 'pie'
|
10
|
+
|
11
|
+
Camping.goes :Charts
|
12
|
+
|
13
|
+
module Charts::Controllers
|
14
|
+
class Index < R '/'
|
15
|
+
def get
|
16
|
+
# find all charts
|
17
|
+
@charts = Dir.glob("charts/*.gif").sort_by{|f|f.match(/(\d+)/)[1].to_i}.reverse
|
18
|
+
|
19
|
+
# keep only ten charts
|
20
|
+
(@charts[10..-1] || []).each{|f|FileUtils.rm(f,:force => true)}
|
21
|
+
@charts = @charts[0..9]
|
22
|
+
|
23
|
+
render :index
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Create < R '/create'
|
28
|
+
def post
|
29
|
+
# get our data
|
30
|
+
slices = input.data.split(',')
|
31
|
+
slices.reject!{|slice| slice !~ /\d+/}
|
32
|
+
slices.map!{|slice| slice.match(/(\d+)/)[1].to_i}
|
33
|
+
slices = [100] if slices.empty?
|
34
|
+
|
35
|
+
data = slices.map{|slice| {:value => slice, :style => "rgb(#{rand(255)},#{rand(255)},#{rand(255)})"}}
|
36
|
+
|
37
|
+
# save our chart
|
38
|
+
chart = Pie.new(data)
|
39
|
+
i = Dir.glob("charts/*.gif").map{|f|f.match(/(\d+)/)[1].to_i + 1}.max || 1
|
40
|
+
chart.draw(25).write("charts/#{i}.gif")
|
41
|
+
|
42
|
+
redirect Index
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Chart < R '/charts/(.+\.gif)'
|
47
|
+
def get filename
|
48
|
+
@headers["Content-Type"] = "image/gif"
|
49
|
+
|
50
|
+
@body = File.read("charts/#{filename}")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module Charts::Views
|
56
|
+
|
57
|
+
def layout
|
58
|
+
html do
|
59
|
+
head do
|
60
|
+
title 'Charts!'
|
61
|
+
end
|
62
|
+
body do
|
63
|
+
div.content do
|
64
|
+
self << yield
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def index
|
71
|
+
form(:method => 'post', :action => '/create') do
|
72
|
+
label do
|
73
|
+
input :name => 'data', :type => 'text', :value => '10,20,30'
|
74
|
+
end
|
75
|
+
input :type => 'submit', :value => 'Prepare a chart'
|
76
|
+
end
|
77
|
+
|
78
|
+
div.charts do
|
79
|
+
@charts.each do |src|
|
80
|
+
img :src => src
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
if __FILE__ == $0
|
88
|
+
Charts.run
|
89
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class Pie
|
2
|
+
|
3
|
+
RADIANS = Math::PI/180
|
4
|
+
MIN_PERCENT = (0.1 / 360.0) * 100
|
5
|
+
MAX_PERCENT = 100 - MIN_PERCENT
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
@data = data
|
9
|
+
end
|
10
|
+
|
11
|
+
def draw(size)
|
12
|
+
position = size / 2
|
13
|
+
offset = (size < 20 ? 1 : size / 20)
|
14
|
+
offset = 5 if offset > 5
|
15
|
+
radius = position - offset
|
16
|
+
|
17
|
+
total = @data.inject(0){|sum, item| sum + item[:value].to_f}
|
18
|
+
percent_scale = 100.0 / total
|
19
|
+
|
20
|
+
full_circle = false
|
21
|
+
angles = [12.5 * 3.6 * RADIANS]
|
22
|
+
slices = []
|
23
|
+
|
24
|
+
@data.each do |item|
|
25
|
+
percent = percent_scale * item[:value].to_f
|
26
|
+
percent = MIN_PERCENT if percent < MIN_PERCENT
|
27
|
+
if percent > MAX_PERCENT
|
28
|
+
full_circle = item
|
29
|
+
else
|
30
|
+
prev_angle = angles.last
|
31
|
+
angles << prev_angle + (percent * 3.6 * RADIANS)
|
32
|
+
slices << {:start => angles[-2], :end => angles[-1], :style => item[:style]}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
rvg = Magick::RVG.new(size,size) do |canvas|
|
37
|
+
canvas.background_fill = 'white'
|
38
|
+
|
39
|
+
# is there a full circle here? then draw it
|
40
|
+
canvas.circle(radius,position,position).styles(:fill => full_circle[:style]) if full_circle
|
41
|
+
|
42
|
+
# draw the fills of the slices
|
43
|
+
slices.each do |slice|
|
44
|
+
canvas.path(slice_path(position,position,radius,slice[:start],slice[:end])).styles(:fill => slice[:style])
|
45
|
+
end
|
46
|
+
|
47
|
+
# outline the graph
|
48
|
+
canvas.circle(radius,position,position).styles(:stroke => 'black', :stroke_width => 0.7, :fill => 'transparent')
|
49
|
+
|
50
|
+
# draw lines between each slice
|
51
|
+
angles[0..-2].each do |a|
|
52
|
+
canvas.line(position, position, position+(Math.sin(a)*radius), position-(Math.cos(a)*radius)).styles(:stroke => 'black', :stroke_width => 0.7, :fill => 'transparent')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
rvg.draw
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def slice_path(x, y, size, start_angle, end_angle)
|
63
|
+
x_start = x+(Math.sin(start_angle) * size)
|
64
|
+
y_start = y-(Math.cos(start_angle) * size)
|
65
|
+
x_end = x+(Math.sin(end_angle) * size)
|
66
|
+
y_end = y-(Math.cos(end_angle) * size)
|
67
|
+
"M#{x},#{y} L#{x_start},#{y_start} A#{size},#{size} 0, #{end_angle - start_angle >= 50 * 3.6 * RADIANS ? '1' : '0'},1, #{x_end} #{y_end} Z"
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
data/examples/serve
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Serves all examples, mounted into Webrick.
|
4
|
+
#
|
5
|
+
require 'stringio'
|
6
|
+
require 'webrick/httpserver'
|
7
|
+
|
8
|
+
dir = Dir.pwd
|
9
|
+
apps =
|
10
|
+
Dir['*'].select do |d|
|
11
|
+
Dir.chdir(dir)
|
12
|
+
if File.exists? "#{d}/#{d}.rb"
|
13
|
+
begin
|
14
|
+
Dir.chdir("#{dir}/#{d}")
|
15
|
+
load "#{d}.rb"
|
16
|
+
true
|
17
|
+
rescue Exception => e
|
18
|
+
puts "Camping app `#{d}' will not load: #{e.class} #{e.message}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
apps.map! do |app|
|
23
|
+
[app, (Object.const_get(Object.constants.grep(/^#{app}$/i)[0]) rescue nil)]
|
24
|
+
end
|
25
|
+
|
26
|
+
s = WEBrick::HTTPServer.new(:BindAddress => '0.0.0.0', :Port => 3301)
|
27
|
+
apps.each do |app, klass|
|
28
|
+
s.mount_proc("/#{app}") do |req, resp|
|
29
|
+
Object.instance_eval do
|
30
|
+
remove_const :ENV
|
31
|
+
const_set :ENV, req.meta_vars
|
32
|
+
end
|
33
|
+
def resp.<<(data)
|
34
|
+
raw_header, body = "#{data}".split(/^[\xd\xa]+/on, 2)
|
35
|
+
|
36
|
+
begin
|
37
|
+
header = WEBrick::HTTPUtils::parse_header(raw_header)
|
38
|
+
if /^(\d+)/ =~ header['status'][0]
|
39
|
+
self.status = $1.to_i
|
40
|
+
header.delete('status')
|
41
|
+
end
|
42
|
+
header.each{|key, val| self[key] = val.join(", ") }
|
43
|
+
rescue => ex
|
44
|
+
raise WEBrick::HTTPStatus::InternalServerError, ex.message
|
45
|
+
end
|
46
|
+
self.body = body
|
47
|
+
end
|
48
|
+
Dir.chdir("#{dir}/#{app}")
|
49
|
+
klass.run((req.body and StringIO.new(req.body)), resp)
|
50
|
+
Dir.chdir(dir)
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
trap(:INT) do
|
55
|
+
s.shutdown
|
56
|
+
end
|
57
|
+
s.start
|