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 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