mwmitchell-pepper 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008 Matt Mitchell - goodieboy@gmail.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,103 @@
1
+ Welcome to the pepper wiki!
2
+ Pepper is a Ruby, DSL based framework for creating RESTful web applications. It adheres to the Ruby Rack specification.
3
+
4
+ Pepper treats every fragment in a URL as a "context". Each context can be mapped to a block or another Pepper enabled module. For example, a URL of:
5
+
6
+ <pre>/users/1/images/1</pre>
7
+
8
+ could translate to a context block tree of:
9
+
10
+ <pre>
11
+ <code>
12
+ map 'users' do
13
+ map :digit do
14
+ get{"The value of this user is #{value}"}
15
+ map 'images' do
16
+ map :digit do
17
+ get{"The ID of this image is #{value}"}
18
+ end
19
+ end
20
+ end
21
+ end
22
+ </code>
23
+ </pre>
24
+
25
+ Because each block maps to a single url fragment, "routing" is not needed. The value of a context's url fragment can be accessed by its :value attribute. You can also name the fragment and propagate to the request params like:
26
+
27
+ <pre>
28
+ <code>
29
+ map :user_id=>/\d+/ do
30
+ puts params[:user_id]
31
+ end
32
+ </code>
33
+ </pre>
34
+
35
+ The :user_id param will then be available as a global request param to other contexts and "actions".
36
+
37
+ The fragment value passed to :map can be a string, a regular expression or a symbol that matches one of the built-in rules; :digit, :word, etc.
38
+
39
+ Standard HTTP methods (GET, POST, PUT, etc.) are used as the "actions". Each of the actions methods can accept a "rule" hash. The keys map to the REQUEST values (but down-cased and symbolized). So you could execute an action only if it matched the IP, or SERVER_NAME. The value of the rule can be a Regular Expression or a string.
40
+
41
+ Here is an example:
42
+
43
+ <pre>
44
+ <code>
45
+ require 'pepper'
46
+
47
+ module SeriousApp
48
+
49
+ class Contact # A delegate/sub-app
50
+
51
+ include Pepper::Context::Delegate
52
+
53
+ build do
54
+ before :post do
55
+ # a before :post block. Default is :all.
56
+ end
57
+
58
+ get{'GET request'}
59
+ post{'POST request'}
60
+
61
+ # request for /contact/:word
62
+ map :word do
63
+ get{"GET request for /contact/#{value}"}
64
+ end
65
+ end
66
+ end
67
+
68
+ class Root
69
+
70
+ include Pepper::App
71
+
72
+ build do
73
+ get{'A simple GET to /'}
74
+ post{'A POST to /'}
75
+
76
+ # /about/
77
+ map 'about' do
78
+ get{'GET request for /about'}
79
+ end
80
+
81
+ # /notes
82
+ map 'notes' do
83
+ get(:server_name=>/^myspecialdomain\.com$/) do
84
+ 'GET request for /notes, but only allowing a SERVER_NAME of "myspecialdomain.com"'}
85
+ end
86
+ end
87
+
88
+ # forwarding /contact to the Contact class
89
+ map 'contact', {}, Contact
90
+ end
91
+
92
+ end
93
+ end
94
+ </code>
95
+ </pre>
96
+
97
+ You then instantiate the Root class, and pass it to Rack's "run" method.
98
+
99
+ <pre>
100
+ <code>
101
+ run SeriousApp::Root.new
102
+ </code>
103
+ </pre>
@@ -0,0 +1,377 @@
1
+ require 'rubygems'
2
+ require 'rack'
3
+
4
+ $:.unshift File.dirname(__FILE__) unless $:.include? File.dirname(__FILE__)
5
+
6
+ module Pepper
7
+
8
+ VERSION='0.3.1'
9
+
10
+ class Request < Rack::Request
11
+ def method_sym
12
+ request_method.to_s.downcase.to_sym
13
+ end
14
+ def path_info_slices
15
+ ['/'] + path_info.to_s.gsub(/\/+/, '/').sub(/^\/|\/$/, '').split('/')
16
+ end
17
+ end
18
+
19
+ module Context
20
+
21
+ class Action
22
+
23
+ #
24
+ # The main context class needs to be able to resfresh this when reloading this action instance
25
+ #
26
+ attr_accessor :app
27
+
28
+ attr :method
29
+ attr :options
30
+ attr :block
31
+
32
+ def initialize(method, app, options={}, &block)
33
+ @app=app
34
+ @method=method
35
+ @options=options
36
+ @block=block
37
+ end
38
+
39
+ def app; @app end
40
+ def request; app.request end
41
+ def params; request.params end
42
+
43
+ def method_missing(m,*args,&block)
44
+ @app.send(m, *args, &block) if [:header, :headers].include?(m)
45
+ end
46
+
47
+ def execute!; instance_eval(&@block) end
48
+
49
+ end
50
+
51
+ #
52
+ #
53
+ #
54
+ module InstanceMethods
55
+
56
+ #
57
+ # The main app class needs to be able to set this
58
+ #
59
+ attr_accessor :app
60
+
61
+ #
62
+ # attributes that are set by the "initialize" method
63
+ #
64
+ # can be a string, regexp, hash or symbol
65
+ # * string - the current path slice will be compared using ==
66
+ # * regexp - the current path slice will be compared using =~
67
+ # * hash - if the value matches, the current slice is assigned to the request.params using the key
68
+ # * the value can in-turn be any string, regexp, hash or symbol
69
+ # * symbol - can be used to match preset rules found in the +PATTERNS+ hash
70
+ # if the symbol is not found in +PATTERNS+, it's converted to a string and uses ==
71
+ #
72
+ attr :pattern
73
+ # The parent context
74
+ attr :parent
75
+ # the options passed into +map+ method calls
76
+ attr :options
77
+ # optional module name to delegate logic for a given sub-path
78
+ attr :delegate
79
+ # the block that holds the executable context code
80
+ attr :block
81
+
82
+ def method_missing(m,*args,&block)
83
+ @app.send(m, *args, &block) if [:header, :headers].include?(m)
84
+ end
85
+
86
+ #
87
+ # Create the HTTP action methods...
88
+ #
89
+ HTTP_METHODS=%W(get post put delete)
90
+ HTTP_METHODS.each do |m|
91
+ class_eval <<-EOF
92
+ def #{m}(options={}, &block)
93
+ if a=actions.detect {|a|a.method==:#{m}.to_sym and a.options==options}
94
+ # refresh the app - this is needed because the root context is a "static" instance
95
+ a.app=@app
96
+ return
97
+ end
98
+ actions << Action.new(:#{m}, @app, options, &block)
99
+ end
100
+ EOF
101
+ end
102
+
103
+ #
104
+ # "attributes" that are set after initialization
105
+ #
106
+
107
+ # returns the child contexts
108
+ def children; @children||=[]; end
109
+ # returns the value from the path slices that this context matched
110
+ def value; @value||=nil; end
111
+ # the base application instance
112
+ def app; @app; end
113
+ # returns instance of Rack::Request
114
+ def request; app.request; end
115
+ # shortcut for request.params
116
+ def params; request.params; end
117
+ #
118
+ def actions; @actions||=[] end
119
+ #
120
+ def before_blocks; @before_blocks||={} end
121
+ #
122
+ def after_blocks; @after_blocks||={} end
123
+
124
+ def before(action=:all, &block)
125
+ before_blocks[action]=block
126
+ end
127
+
128
+ def after(action=:all, &block)
129
+ after_blocks[action]=block
130
+ end
131
+
132
+ #
133
+ # Finds the action (get, post, put etc.) base on:
134
+ # * the method
135
+ # * the env/hash values
136
+ #
137
+ def find_action(method, env={})
138
+ actions.each do |a|
139
+ next if a.method != method
140
+ return a if a.options.nil? or a.options.size==0
141
+ a.options.each_pair do |k,v|
142
+ k=k.to_s.upcase
143
+ return a if env[k]==v or env[k]=~v
144
+ end
145
+ end
146
+ nil
147
+ end
148
+
149
+ PATTERNS={
150
+ :digit=>/^\d+$/,
151
+ :word=>/^\w+$/
152
+ }
153
+
154
+ #
155
+ #
156
+ #
157
+ def match?(pattern, value)
158
+ ok = simple_match?(pattern, value)
159
+ return ok if ok
160
+ # if hash, use the value as the pattern and set the request param[key] to the value argument
161
+ return (params[pattern.keys.first] = match?(pattern[pattern.keys.first], value)) if pattern.is_a?(Hash)
162
+ # if symbol
163
+ if pattern.is_a?(Symbol)
164
+ # lookup the symbol in the PATTERNS hash
165
+ return value if PATTERNS[pattern] and match?(PATTERNS[pattern], value)
166
+ # There was no match, convert to string
167
+ simple_match?(pattern.to_s, value)
168
+ end
169
+ end
170
+
171
+ #
172
+ #
173
+ #
174
+ def simple_match?(pattern, value)
175
+ # if string, simple comparison
176
+ return value if (pattern.is_a?(String) and value == pattern)
177
+ # if regexp, regx comparison
178
+ return value if (pattern.is_a?(Regexp) and value =~ pattern)
179
+ end
180
+
181
+ #
182
+ # Recursively looks for matching context instances returning the last one that matches.
183
+ #
184
+ def resolve(slices)
185
+ return unless match?(pattern, slices.first)
186
+ @value=slices.shift
187
+ catch :delegate do
188
+ instance_eval(&@block) if @block
189
+ end
190
+ if delegate
191
+ raise 'Only module based delegates are allowed' unless delegate.class==Module
192
+ return delegate.delegate!(self, [@value]+slices)
193
+ end
194
+ return self if slices.size==0
195
+ children.each do |child|
196
+ if found=child.resolve(slices)
197
+ return found
198
+ end
199
+ end
200
+ nil
201
+ end
202
+
203
+ #
204
+ # Shortcut assigning a delegate
205
+ #
206
+ def delegate_to(handler)
207
+ @delegate=handler
208
+ throw :delegate
209
+ end
210
+
211
+ def child_exists?(pattern, options)
212
+ children.detect{|child|child.pattern==pattern and child.options==options}
213
+ end
214
+
215
+ #
216
+ # The main method for creating child contexts
217
+ #
218
+ def map(pattern, options={}, delegate=nil, &block)
219
+ if child=child_exists?(pattern, options)
220
+ # refresh the app
221
+ child.app=@app
222
+ return child
223
+ else
224
+ children << Context::Base.new(pattern, options, delegate, self, &block)
225
+ children.last.app=app
226
+ end
227
+ end
228
+
229
+ end # end InstanceMethods
230
+
231
+ #
232
+ #
233
+ #
234
+
235
+ #
236
+ # Used for handling areas of your application, within the context of some path.
237
+ # For example, if you had a blog area of your site, you could off-load the logic
238
+ # from the main app to a "delegate" and keep the main app clear of too much clutter.
239
+ # It would actually be possible for your main app to do nothing but delegate.
240
+ #
241
+ module Delegate
242
+
243
+ #
244
+ # Shortcut for:
245
+ # extend Pepper::Context::Delegate::ClassMethods
246
+ #
247
+ def self.included(base)
248
+ base.extend ClassMethods
249
+ end
250
+
251
+ #
252
+ # Pepper::Context::Delegate::ClassMethods
253
+ #
254
+ module ClassMethods
255
+
256
+ #
257
+ # Bring in the standard context methods
258
+ #
259
+ include Context::InstanceMethods
260
+
261
+ #
262
+ # define the standard build method so the delegate can start creating the context tree
263
+ #
264
+ def build(options={}, &block)
265
+ @options=options
266
+ @block=block
267
+ end
268
+
269
+ #
270
+ # Sets up this module as a delegate handler
271
+ # delegating_for - Pepper::Context::Base instance
272
+ # slices - array of path fragments
273
+ #
274
+ def delegate!(delegating_for, slices)
275
+ @app=delegating_for.app
276
+ @parent=delegating_for.parent
277
+ # Set the @pattern to the delegating_for's value so that when resolve is called, there will be a match
278
+ @pattern=delegating_for.value
279
+ # add this delegate to the delegating_for.parent.children
280
+ delegating_for.parent.children.unshift self
281
+ # find the matching context based on the slices
282
+ return resolve(slices)
283
+ end
284
+ end
285
+
286
+ end
287
+
288
+ #
289
+ # The base context class that gets instantiated for each "map" call
290
+ #
291
+ class Base
292
+
293
+ include Context::InstanceMethods
294
+
295
+ #
296
+ #
297
+ #
298
+ def initialize(pattern, options={}, delegate=nil, parent=nil, &block)
299
+ @pattern=pattern
300
+ @options=options
301
+ @delegate=delegate
302
+ @block=block
303
+ @parent=parent
304
+ end
305
+
306
+ end
307
+
308
+ end
309
+
310
+ #########
311
+
312
+ module App
313
+
314
+ #
315
+ # Shortcut for:
316
+ # include Pepper::App::InstanceMethods
317
+ # extend Pepper::App::ClassMethods
318
+ #
319
+ def self.included(base)
320
+ base.extend ClassMethods
321
+ base.send :include, InstanceMethods
322
+ end
323
+
324
+ #
325
+ # The class methods; provides the main "build" method
326
+ #
327
+ module ClassMethods
328
+
329
+ attr :root
330
+
331
+ #
332
+ # The initial build, class method
333
+ #
334
+ def build(options={}, &block)
335
+ @root=Context::Base.new('/', options, nil, nil, &block)
336
+ end
337
+
338
+ end
339
+
340
+ #
341
+ # The interface for Rack to connect to using the +call+ method
342
+ #
343
+ module InstanceMethods
344
+
345
+ class Error404 < RuntimeError; end
346
+ class ErrorMethodNotSupported < RuntimeError; end
347
+ class ErrorRootContextNotFound < RuntimeError; end
348
+
349
+ def root; self.class.root end
350
+ def request; @request end
351
+
352
+ def response; @response; end
353
+
354
+ def header(name, value)
355
+ response[name]=value
356
+ end
357
+
358
+ def headers(hash)
359
+ hash.each_pair {|k,v|header(k,v)}
360
+ end
361
+
362
+ def call(rack_env)
363
+ @response=Rack::Response.new
364
+ @request=Request.new(rack_env)
365
+ raise ErrorRootContextNotFound if root.nil?
366
+ root.app=self
367
+ context = root.resolve(request.path_info_slices)
368
+ raise Error404 if context.nil?
369
+ action = context.find_action(request.method_sym, request.env)
370
+ raise ErrorMethodNotSupported if action.nil?
371
+ response.write action.execute!
372
+ response.finish
373
+ end
374
+ end
375
+ end
376
+
377
+ end
@@ -0,0 +1,54 @@
1
+ module Pepper::Demo
2
+
3
+ module Canning
4
+ include Pepper::Context::Delegate
5
+ build do
6
+ get do
7
+ 'CANNING PEPPERS: Small peppers may be left whole; large peppers may be quartered. Remove cores and seeds; blister and peel peppers; flatten whole peppers. Add ½ teaspoon of salt to each pint jar, if desired. Fill jars loosely with peppers and add fresh boiled water, leaving 1-inch headspace. Adjust lids and process half-pints or pints for 35 minutes at 11 pounds pressure in a dial gauge canner or at 10 pounds pressure in a weighted gauge canner.'
8
+ end
9
+ end
10
+ end
11
+
12
+ class Pickled
13
+
14
+ include Pepper::App
15
+
16
+ build do
17
+
18
+ header 'Content-Type', 'text/html'
19
+
20
+ get do
21
+ header 'Content-Type', 'text/plain'
22
+ 'CAUTION: When working with hot peppers, wear plastic gloves while handling them, or wash hands thoroughly with soap and water before touching your face.'
23
+ end
24
+
25
+ post{'We are creating a pickled pepper just for you. You will learn to love her.'}
26
+ put{'Adding a little more vinegar... removing some fuzz here and there.'}
27
+ delete{'Bad! Bad! Bad pickled pepper!'}
28
+
29
+ map :peter do
30
+ get{"I am Peter, the pickled-pepper-packer, I want to tell you my story."}
31
+ map :piper do
32
+ map :packed do
33
+ map :a do
34
+ map :pack do
35
+ map :of do
36
+ map :pickled do
37
+ map :peppers do
38
+ get{'zwiggle'}
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ map(:how_to_can) {delegate_to Canning}
49
+
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,7 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'pepper'
3
+ require 'spec'
4
+
5
+ def new_rack_env(path='/', attrs={})
6
+ Rack::MockRequest.env_for(path, attrs)
7
+ end
@@ -0,0 +1,69 @@
1
+ require File.join(File.dirname(__FILE__), 'common.rb')
2
+ require 'pepper/demo'
3
+
4
+ #
5
+ #
6
+ #
7
+ describe Pepper::Demo::Pickled do
8
+
9
+ before :each do
10
+ @app_class = Pepper::Demo::Pickled
11
+ @app=@app_class.new
12
+ end
13
+
14
+ it '(class) should respond to build' do
15
+ @app_class.should respond_to(:build)
16
+ end
17
+
18
+ it "should respond to :call" do
19
+ @app.should respond_to(:call)
20
+ end
21
+
22
+ it "should have a nil :request attribute until :call is executed" do
23
+ @app.request.should == nil
24
+ end
25
+
26
+ it "should have a :request attribute after calling the call method" do
27
+ @app.call(new_rack_env)
28
+ @app.should respond_to(:request)
29
+ @app.request.class.should == Pepper::Request
30
+ end
31
+
32
+ it "should not fail when requesting a POST, GET, PUT or DELETE to /" do
33
+ r=@app.call(new_rack_env)
34
+ r[0].should == 200
35
+ r[1].should == {'Content-Type'=>'text/plain'}
36
+ r[2].body.should == ['CAUTION: When working with hot peppers, wear plastic gloves while handling them, or wash hands thoroughly with soap and water before touching your face.']
37
+ #
38
+ r=@app.call(new_rack_env('/', {'REQUEST_METHOD'=>'POST'}))
39
+ r[0].should == 200
40
+ r[1].should == {'Content-Type'=>'text/html'}
41
+ r[2].body.should == ['We are creating a pickled pepper just for you. You will learn to love her.']
42
+ #
43
+ r=@app.call(new_rack_env('/', {'REQUEST_METHOD'=>'PUT'}))
44
+ r[0].should == 200
45
+ r[1].should == {'Content-Type'=>'text/html'}
46
+ r[2].body.should == ['Adding a little more vinegar... removing some fuzz here and there.']
47
+ #
48
+ r=@app.call(new_rack_env('/', {'REQUEST_METHOD'=>'DELETE'}))
49
+ r[0].should == 200
50
+ r[1].should == {'Content-Type'=>'text/html'}
51
+ r[2].body.should == ['Bad! Bad! Bad pickled pepper!']
52
+ end
53
+
54
+ it "should raise Error404 when requesting an invalid path" do
55
+ lambda {@app.call(new_rack_env('/blah'))}.should raise_error(@app.class::Error404)
56
+ end
57
+
58
+ it "should raise ErrorMethodNotSupported when requesting a valid path, but no matching request method action" do
59
+ lambda {@app.call(new_rack_env('/peter', {'REQUEST_METHOD'=>'DELETE'}))}.should raise_error(@app.class::ErrorMethodNotSupported)
60
+ end
61
+
62
+ it 'should support unlimited nesting depth' do
63
+ @app.call(new_rack_env('/peter/piper/packed/a/pack/of/pickled/peppers'))[2].body.should == ['zwiggle']
64
+ end
65
+
66
+ it 'should support delegating from one context block to another' do
67
+ @app.call(new_rack_env('/how_to_can'))[2].body.should == ['CANNING PEPPERS: Small peppers may be left whole; large peppers may be quartered. Remove cores and seeds; blister and peel peppers; flatten whole peppers. Add ½ teaspoon of salt to each pint jar, if desired. Fill jars loosely with peppers and add fresh boiled water, leaving 1-inch headspace. Adjust lids and process half-pints or pints for 35 minutes at 11 pounds pressure in a dial gauge canner or at 10 pounds pressure in a weighted gauge canner.']
68
+ end
69
+ end
@@ -0,0 +1,23 @@
1
+ require File.join(File.dirname(__FILE__), 'common.rb')
2
+
3
+ #
4
+ #
5
+ #
6
+ describe Pepper::Request do
7
+ it '(path_info_fragments return) should return an array' do
8
+ r=Pepper::Request.new(new_rack_env('/'))
9
+ r.path_info_slices.class.should == Array
10
+ end
11
+
12
+ it '(path_info_fragments return) should always start with a /' do
13
+ r=Pepper::Request.new(new_rack_env('a/test//to/a/path/'))
14
+ r.path_info_slices[0].should == '/'
15
+ r=Pepper::Request.new(new_rack_env('/a/test//to/a/path/'))
16
+ r.path_info_slices[0].should == '/'
17
+ end
18
+
19
+ it 'should remove duplicate slashes when responding to path_info_fragments' do
20
+ r=Pepper::Request.new(new_rack_env('a/test//to/a/path/'))
21
+ r.path_info_slices.should == ['/', 'a', 'test', 'to', 'a', 'path']
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mwmitchell-pepper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Mitchell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-06-11 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - "="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.3.0
23
+ version:
24
+ description: Pepper is a Ruby, DSL based framework for creating RESTful web applications. It adheres to the Ruby Rack specification.
25
+ email: goodieboy@gmail.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - README
32
+ - LICENSE
33
+ files:
34
+ - lib/pepper.rb
35
+ - lib/pepper/demo.rb
36
+ - README
37
+ - LICENSE
38
+ has_rdoc: true
39
+ homepage: http://github.com/mwmitchell/pepper
40
+ post_install_message:
41
+ rdoc_options:
42
+ - --main
43
+ - README
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.0.1
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: Pepper is a Ruby, DSL based framework for creating RESTful web applications. It adheres to the Ruby Rack specification.
65
+ test_files:
66
+ - spec/common.rb
67
+ - spec/demo.spec.rb
68
+ - spec/request.spec.rb