camping 1.1 → 1.2

Sign up to get free protection for your applications and to get access to all the features.
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>
@@ -1,16 +1,30 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- #$:.unshift File.dirname(__FILE__) + "/../../lib"
3
+ $:.unshift File.dirname(__FILE__) + "/../../lib"
4
4
  require 'rubygems'
5
5
  require 'camping'
6
6
 
7
- module Camping::Models
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
- module Camping::Controllers
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
- pre cookies.inspect
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 Camping::Views
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 => '/add' }
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 => '/add' }
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 => '/add')
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 => '/edit')
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 => '/comment', :method => 'post' do
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 => '/login', :method => 'post' do
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 => "/edit/#{post.id}"
194
- a "View", :href => "/view/#{post.id}"
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 => '/logout'
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
- Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/ruby
2
+ require 'webrick/httpserver'
3
+ s = WEBrick::HTTPServer.new(:BindAddress => 'localhost', :Port => 3044)
4
+ s.mount("/", WEBrick::HTTPServlet::CGIHandler, "charts.rb")
5
+ trap( :INT ) { s.shutdown }
6
+ s.start
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