mikeycgto-sinatra-rest 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile ADDED
@@ -0,0 +1,90 @@
1
+ h1. Sinatra-REST
2
+
3
+ Actually it's a set of templates to introduce RESTful routes in Sinatra. The
4
+ only thing for you to do is to provide the views. The routes and some
5
+ url helpers will be provided behind the scenes.
6
+
7
+
8
+ h2. Installation
9
+
10
+ Guess what!
11
+
12
+ sudo gem source --add http://gems.github.com
13
+ sudo gem install blindgaenger-sinatra-rest
14
+
15
+
16
+ h2. Usage
17
+
18
+ Of course you need to require the gem in your Sinatra application:
19
+
20
+ require 'rubygems'
21
+ require 'sinatra'
22
+ require 'sinatra/rest'
23
+
24
+ It's very similar to defining routes in Sinatra (@get@, @post@, ...). But this
25
+ time you don't define the routes by yourself, but use the model's name for
26
+ convention.
27
+
28
+ For example, if the model's class is called @Person@ you only need to add this
29
+ line:
30
+
31
+ rest Person
32
+
33
+ Which will add the following RESTful routes to your application. (Note the
34
+ pluralization of @Person@ to the @/people/*@ routes.)
35
+
36
+ * GET /people
37
+ * GET /people/new
38
+ * POST /people
39
+ * GET /people/:id
40
+ * GET /people/:id/edit
41
+ * PUT /people/:id
42
+ * DELETE /people/:id
43
+
44
+ But the real benefit is, that these *routes define a restful standard behaviour*
45
+ on your model, *appropriate routing and redirecting* and *named url helpers*.
46
+
47
+ For instance, you can imagine the following code to be added for the @/people@
48
+ and @/people/:id@ routes.
49
+
50
+ <pre><code>
51
+ # simply add this line
52
+
53
+ rest Person, :renderer => :erb
54
+
55
+ # and this is generated for you
56
+
57
+ get '/people' do
58
+ @people = Person.all
59
+ erb :"people/index", options
60
+ end
61
+
62
+ put '/people/:id' do
63
+ @person = Person.find_by_id(params[:id])
64
+ redirect url_for_people_show(@person), 'person updated'
65
+ end
66
+
67
+ # further restful routes for Person ...
68
+ </code></pre>
69
+
70
+ That's only half the truth! The routes are generated dynamically, so all
71
+ defaults can be overridden (the behaviour, after/before callbacks, used renderer,
72
+ which routes are added).
73
+
74
+ For more details and options, please have a look at the pages in the
75
+ "Sinatra-REST Wiki":http://github.com/blindgaenger/sinatra-rest/wikis on Github.
76
+
77
+
78
+ h2. Links
79
+
80
+ * "Homepage @ GitHub Pages":http://blindgaenger.github.com/sinatra-rest/
81
+ * "Source Code @ GitHub":http://blindgaenger.github.com/sinatra-rest/
82
+ * "Documentation @ rdoc.info":http://rdoc.info/projects/blindgaenger/sinatra-rest
83
+ * "Continuous Integration @ RunCodeRun":http://runcoderun.com/blindgaenger/sinatra-rest
84
+ * "Gem hosting @ Gemcutter":http://gemcutter.org/gems/sinatra-rest
85
+
86
+ h2. Contact
87
+
88
+ You can contact me via mail at blindgaenger at gmail dot com, or leave me a
89
+ message on my "Github profile":http://github.com/blindgaenger.
90
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'spec/rake/spectask'
2
+
3
+ task :default => :test
4
+
5
+ desc "Run tests"
6
+ Spec::Rake::SpecTask.new :test do |t|
7
+ t.spec_opts = %w(--format specdoc --color) #--backtrace
8
+ t.spec_files = FileList['test/*_spec.rb']
9
+ end
10
+
@@ -0,0 +1,53 @@
1
+ module Stone
2
+ module Resource
3
+ def find_by_id(id)
4
+ get(id)
5
+ end
6
+
7
+ def delete(id)
8
+ model = self.find_by_id(id)
9
+ model.destroy if model
10
+ end
11
+ end
12
+ end
13
+
14
+ module DataMapper
15
+
16
+ module Resource
17
+ def update_attributes(opt)
18
+ update opt
19
+ end
20
+ end
21
+
22
+ module Model
23
+ def find_by_id(id)
24
+ get(id)
25
+ end
26
+
27
+ def delete(id)
28
+ model = self.find_by_id(id)
29
+ model.destroy if model
30
+ end
31
+ end
32
+ end
33
+
34
+ # find throws exceptions...
35
+ # find_by_id returns nil
36
+ #
37
+ # find_by_id should already exist but alas it doesn't
38
+ # because templates are eval'd before that method is.. (i think)
39
+ module ActiveRecord
40
+ class Base
41
+ class << self
42
+ def find_by_id(id)
43
+ begin
44
+ find(id)
45
+ rescue Exception
46
+ nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+
@@ -0,0 +1,84 @@
1
+ ---
2
+ :index:
3
+ :verb: GET
4
+ :url: /PLURAL
5
+ :control: |-
6
+ @PLURAL = call_model_method(MODEL, :where, mp)
7
+ :render: |-
8
+ if request.xhr?
9
+ content_type :json
10
+ @PLURAL.as_json.to_json
11
+ else
12
+ RENDERER :'PLURAL/index', options
13
+ end
14
+
15
+ :new:
16
+ :verb: GET
17
+ :url: /PLURAL/new
18
+ :control: |-
19
+ @SINGULAR = call_model_method(MODEL, :new, mp)
20
+ :render: |-
21
+ RENDERER :'PLURAL/new', options
22
+
23
+ :create:
24
+ :verb: POST
25
+ :url: /PLURAL
26
+ :control: |-
27
+ @SINGULAR = call_model_method(MODEL, :new, mp)
28
+ @SINGULAR.save
29
+ :render: |-
30
+ unless @SINGULAR.invalid?
31
+ redirect url_for_PLURAL_show(@SINGULAR), 'SINGULAR created'
32
+ else
33
+ throw :halt, [500, @SINGULAR.errors.full_messages.join("\n")]
34
+ end
35
+
36
+ :show:
37
+ :verb: GET
38
+ :url: /PLURAL/:id
39
+ :control: |-
40
+ @SINGULAR = call_model_method(MODEL, :find_by_id, mp[:id])
41
+ :render: |-
42
+ if @SINGULAR.nil?
43
+ throw :halt, [404, 'SINGULAR not found']
44
+ else
45
+ if request.xhr?
46
+ content_type :json
47
+ @SINGULAR.as_json.to_json
48
+ else
49
+ RENDERER :'PLURAL/show', options
50
+ end
51
+ end
52
+
53
+ :edit:
54
+ :verb: GET
55
+ :url: /PLURAL/:id/edit
56
+ :control: |-
57
+ @SINGULAR = call_model_method(MODEL, :find_by_id, mp[:id])
58
+ :render: |-
59
+ RENDERER :'PLURAL/edit', options
60
+
61
+ :update:
62
+ :verb: PUT
63
+ :url: /PLURAL/:id
64
+ :control: |-
65
+ @SINGULAR = call_model_method(MODEL, :find_by_id, mp[:id])
66
+ @SINGULAR.update_attributes(mp) unless @SINGULAR.nil?
67
+ :render: |-
68
+ if @SINGULAR.nil?
69
+ throw :halt, [404, 'SINGULAR not found']
70
+ else
71
+ unless @SINGULAR.invalid?
72
+ redirect url_for_PLURAL_show(@SINGULAR), 'SINGULAR updated'
73
+ else
74
+ throw :halt, [500, @SINGULAR.errors.full_messages.join("\n")]
75
+ end
76
+ end
77
+
78
+ :destroy:
79
+ :verb: DELETE
80
+ :url: /PLURAL/:id
81
+ :control: |-
82
+ call_model_method(MODEL, :delete, mp[:id])
83
+ :render: |-
84
+ redirect url_for_PLURAL_index, 'SINGULAR destroyed'
@@ -0,0 +1,209 @@
1
+ require 'sinatra/base'
2
+ require 'english/inflect'
3
+
4
+ libdir = File.dirname(__FILE__) + "/rest"
5
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
6
+ require 'adapters'
7
+ require 'yaml'
8
+
9
+ module Sinatra
10
+
11
+ module REST
12
+
13
+ #
14
+ # adds restful routes and url helpers for the model
15
+ def rest(model_class, options={}, &block)
16
+ parse_args(model_class, options)
17
+ read_config('rest/rest.yaml')
18
+
19
+ # register model specific helpers
20
+ helpers generate_helpers
21
+
22
+ # create an own module, to override the template with custom methods
23
+ # this way, you can still use #super# in the overridden methods
24
+ controller = generate_controller
25
+ if block_given?
26
+ custom = CustomController.new(@plural)
27
+ custom.instance_eval &block
28
+ custom.module.send :include, controller
29
+ controller = custom.module
30
+ end
31
+ helpers controller
32
+
33
+ # register routes as DSL extension
34
+ instance_eval generate_routes
35
+ end
36
+
37
+ protected
38
+
39
+ ROUTES = {
40
+ :all => [:index, :new, :create, :show, :edit, :update, :destroy],
41
+ :readable => [:index, :show],
42
+ :writeable => [:index, :show, :create, :update, :destroy],
43
+ :editable => [:index, :show, :create, :update, :destroy, :new, :edit],
44
+ }
45
+
46
+ def parse_args(model_class, options)
47
+ @model, @singular, @plural = conjugate(model_class)
48
+ @renderer = (options.delete(:renderer) || :haml).to_s
49
+ @route_flags = parse_routes(options.delete(:routes) || :all)
50
+ end
51
+
52
+ def parse_routes(routes)
53
+ routes = [*routes].map {|route| ROUTES[route] || route}.flatten.uniq
54
+ # keep the order of :all routes
55
+ ROUTES[:all].select{|route| routes.include? route}
56
+ end
57
+
58
+ def read_config(filename)
59
+ file = File.read(File.join(File.dirname(__FILE__), filename))
60
+ @config = YAML.load file
61
+ end
62
+
63
+ #
64
+ # creates the necessary forms of the model name
65
+ # pretty much like ActiveSupport's inflections, but don't like to depend on
66
+ def conjugate(model_class)
67
+ model = model_class.to_s.match(/(\w+)$/)[0]
68
+ singular = model.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
69
+ return model, singular, English::Inflect.plural(singular)
70
+ end
71
+
72
+ def replace_variables(t, route=nil)
73
+ if route
74
+ t.gsub!('NAME', route.to_s)
75
+ t.gsub!('VERB', @config[route][:verb].downcase)
76
+ t.gsub!('URL', @config[route][:url])
77
+ t.gsub!('CONTROL', @config[route][:control])
78
+ t.gsub!('RENDER', @config[route][:render])
79
+ end
80
+ t.gsub!(/PLURAL/, @plural)
81
+ t.gsub!(/SINGULAR/, @singular)
82
+ t.gsub!(/MODEL/, @model)
83
+ t.gsub!(/RENDERER/, @renderer)
84
+ t
85
+ end
86
+
87
+ def generate_routes
88
+ @route_flags.map{|r| route_template(r)}.join("\n\n")
89
+ end
90
+
91
+ def route_template(route)
92
+ t = <<-RUBY
93
+ VERB 'URL' do
94
+ PLURAL_before :NAME
95
+ PLURAL_NAME
96
+ PLURAL_after :NAME
97
+ RENDER
98
+ end
99
+ RUBY
100
+ replace_variables(t, route)
101
+ end
102
+
103
+ def generate_helpers
104
+ m = Module.new
105
+ @route_flags.each {|r|
106
+ m.module_eval helpers_template(r)
107
+ }
108
+ m
109
+ end
110
+
111
+ def helpers_template(route)
112
+ t = <<-RUBY
113
+ def url_for_PLURAL_NAME(model = nil)
114
+ "URL"
115
+ end
116
+ RUBY
117
+ helper_route = @config[route][:url].gsub(':id', '#{escape_model_id(model)}')
118
+ t.gsub!('URL', helper_route)
119
+ replace_variables(t, route)
120
+ end
121
+
122
+ def generate_controller
123
+ m = Module.new
124
+ t = <<-RUBY
125
+ def PLURAL_before(name); end
126
+ def PLURAL_after(name); end
127
+ RUBY
128
+ m.module_eval replace_variables(t)
129
+
130
+ @route_flags.each {|route|
131
+ m.module_eval controller_template(route)
132
+ }
133
+ m
134
+ end
135
+
136
+ def controller_template(route)
137
+ t = <<-RUBY
138
+ def PLURAL_NAME(options=params)
139
+ mp = filter_model_params(options)
140
+ CONTROL
141
+ end
142
+ RUBY
143
+ replace_variables(t, route)
144
+ end
145
+
146
+ #
147
+ # model unspecific helpers, will be included once
148
+ module Helpers
149
+ # for example _method will be removed
150
+ def filter_model_params(params)
151
+ params.reject {|k, v| k =~ /^(_|session_token)/ }
152
+ end
153
+
154
+ def escape_model_id(model)
155
+ if model.nil?
156
+ raise 'can not generate url for nil'
157
+ elsif model.kind_of?(String)
158
+ Rack::Utils.escape(model)
159
+ elsif model.kind_of?(Fixnum)
160
+ model
161
+ elsif model.id.kind_of? String
162
+ Rack::Utils.escape(model.id)
163
+ else
164
+ model.id
165
+ end
166
+ end
167
+
168
+ def call_model_method(model_class, name, options={})
169
+ if options.nil? || model_class.method(name).arity == 0
170
+ Kernel.warn "warning: calling #{model_class.to_s}##{name} with args, although it doesn't take args" if options
171
+ model_class.send(name)
172
+ else
173
+ model_class.send(name, options)
174
+ end
175
+ end
176
+ end
177
+
178
+ #
179
+ # used as context to evaluate the controller's module
180
+ class CustomController
181
+ attr_reader :module
182
+
183
+ def initialize(prefix)
184
+ @prefix = prefix
185
+ @module = Module.new
186
+ end
187
+
188
+ def before(options={}, &block) prefix :before, &block; end
189
+ def after(options={}, &block) prefix :after, &block; end
190
+ def index(options={}, &block) prefix :index, &block; end
191
+ def new(options={}, &block) prefix :new, &block; end
192
+ def create(options={}, &block) prefix :create, &block; end
193
+ def show(options={}, &block) prefix :show, &block; end
194
+ def edit(options={}, &block) prefix :edit, &block; end
195
+ def update(options={}, &block) prefix :update, &block; end
196
+ def destroy(options={}, &block) prefix :destroy, &block; end
197
+
198
+ private
199
+ def prefix(name, &block)
200
+ @module.send :define_method, "#{@prefix}_#{name}", &block if block_given?
201
+ end
202
+ end
203
+
204
+ end # REST
205
+
206
+ helpers REST::Helpers
207
+ register REST
208
+
209
+ end # Sinatra
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+ #require 'dm-core'
3
+
4
+ class Word
5
+ include DataMapper::Resource
6
+ property :id, Serial
7
+ property :name, String
8
+ end
9
+
10
+ describe 'with datamappper model' do
11
+
12
+ before(:all) do
13
+ DataMapper.setup(:default, 'sqlite3::memory:')
14
+ DataMapper.auto_migrate!
15
+ @existing_word = Word.create :name => 'existing'
16
+ mock_rest Word
17
+ end
18
+
19
+ it 'should load new' do
20
+ get '/words/new'
21
+ last_response.status.should == 200
22
+ last_response.body.should_not be_empty
23
+ end
24
+
25
+ it 'should create record and redirect to show' do
26
+ post '/words', {:name => 'paleontologist'}
27
+ word = Word.first(:name => 'paleontologist')
28
+ word.should_not be_nil
29
+ last_response.status.should == 302
30
+ last_response.headers['Location'].should == "/words/#{word.id}"
31
+ end
32
+
33
+ it 'should load show' do
34
+ get "/words/#{@existing_word.id}"
35
+ last_response.status.should == 200
36
+ last_response.body.should match @existing_word.name
37
+ end
38
+
39
+ it 'should load edit' do
40
+ get "/words/#{@existing_word.id}/edit"
41
+ last_response.status.should == 200
42
+ last_response.body.should match @existing_word.name
43
+ end
44
+
45
+ it 'should update record and redirect to show' do
46
+ put "/words/#{@existing_word.id}", {:name => 'changed'}
47
+ changed_word = Word.first(:name => 'changed')
48
+ changed_word.should_not be_nil
49
+ last_response.status.should == 302
50
+ last_response.headers['Location'].should == "/words/#{changed_word.id}"
51
+ changed_word.update :name => 'existing'
52
+ end
53
+
54
+ it 'should delete record' do
55
+ word = Word.create :name => 'delete_me'
56
+ delete "/words/#{word.id}"
57
+ Word.get(:name => 'delete_me').should be_nil
58
+ last_response.status.should == 302
59
+ last_response.headers['Location'].should == '/words'
60
+ end
61
+
62
+ end
@@ -0,0 +1,111 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ describe 'call order' do
4
+
5
+ def called_routes
6
+ @app.call_order.map {|r, m| r}.uniq
7
+ end
8
+
9
+ def called_methods
10
+ @app.call_order.map {|r, m| m}
11
+ end
12
+
13
+ before(:each) do
14
+ mock_app {
15
+ configure do
16
+ set :call_order, []
17
+ end
18
+
19
+ rest Person do
20
+ before do |route|
21
+ options.call_order << [route, :before]
22
+ super
23
+ end
24
+
25
+ after do |route|
26
+ options.call_order << [route, :after]
27
+ super
28
+ end
29
+
30
+ index do
31
+ options.call_order << [:index, :index]
32
+ super
33
+ end
34
+
35
+ new do
36
+ options.call_order << [:new, :new]
37
+ super
38
+ end
39
+
40
+ create do
41
+ options.call_order << [:create, :create]
42
+ super
43
+ end
44
+
45
+ show do
46
+ options.call_order << [:show, :show]
47
+ super
48
+ end
49
+
50
+ edit do
51
+ options.call_order << [:edit, :edit]
52
+ super
53
+ end
54
+
55
+ update do
56
+ options.call_order << [:update, :update]
57
+ super
58
+ end
59
+
60
+ destroy do
61
+ options.call_order << [:destroy, :destroy]
62
+ super
63
+ end
64
+ end
65
+ }
66
+ end
67
+
68
+ it 'should call :index in the right order' do
69
+ index '/people'
70
+ called_methods.should == [:before, :index, :after]
71
+ called_routes.should == [:index]
72
+ end
73
+
74
+ it 'should call :new in the right order' do
75
+ new '/people/new'
76
+ called_methods.should == [:before, :new, :after]
77
+ called_routes.should == [:new]
78
+ end
79
+
80
+ it 'should call :create in the right order' do
81
+ create('/people', :name => 'initial name')
82
+ called_methods.should == [:before, :create, :after]
83
+ called_routes.should == [:create]
84
+ end
85
+
86
+ it 'should call :show in the right order' do
87
+ show '/people/1'
88
+ called_methods.should == [:before, :show, :after]
89
+ called_routes.should == [:show]
90
+ end
91
+
92
+ it 'should call :edit in the right order' do
93
+ edit '/people/1/edit'
94
+ called_methods.should == [:before, :edit, :after]
95
+ called_routes.should == [:edit]
96
+ end
97
+
98
+ it 'should call :update in the right order' do
99
+ update '/people/1', :name => 'new name'
100
+ called_methods.should == [:before, :update, :after]
101
+ called_routes.should == [:update]
102
+ end
103
+
104
+ it 'should call :destroy in the right order' do
105
+ destroy '/people/1'
106
+ called_methods.should == [:before, :destroy, :after]
107
+ called_routes.should == [:destroy]
108
+ end
109
+
110
+ end
111
+
data/test/crud_spec.rb ADDED
@@ -0,0 +1,98 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ describe 'some use cases' do
4
+
5
+ def total_models
6
+ Person.all.size
7
+ end
8
+
9
+ require "rexml/document"
10
+ def doc(xml)
11
+ REXML::Document.new(xml.gsub(/>\s+</, '><').strip)
12
+ end
13
+
14
+ before(:each) do
15
+ Person.reset!
16
+ mock_rest Person
17
+ end
18
+
19
+ it 'should list all persons' do
20
+ get '/people'
21
+ normalized_response.should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
22
+ total_models.should == 3
23
+ end
24
+
25
+ it 'should create a new person' do
26
+ get '/people'
27
+ normalized_response.should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
28
+ total_models.should == 3
29
+
30
+ get '/people/new'
31
+ normalized_response.should == [200, '<person><id></id><name></name></person>']
32
+ total_models.should == 3
33
+
34
+ post '/people', {:name => 'four'}
35
+ normalized_response.should == [302, 'person created']
36
+ total_models.should == 4
37
+
38
+ get '/people'
39
+ normalized_response.should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person><person><id>4</id></person></people>']
40
+ total_models.should == 4
41
+ end
42
+
43
+ it 'should read all persons' do
44
+ get '/people'
45
+ el_people = doc(last_response.body).elements.to_a("*/person/id")
46
+ el_people.size.should == 3
47
+ total_models.should == 3
48
+
49
+ get "/people/#{el_people[0].text}"
50
+ normalized_response.should == [200, '<person><id>1</id><name>one</name></person>']
51
+ total_models.should == 3
52
+
53
+ get "/people/#{el_people[1].text}"
54
+ normalized_response.should == [200, '<person><id>2</id><name>two</name></person>']
55
+ total_models.should == 3
56
+
57
+ get "/people/#{el_people[2].text}"
58
+ normalized_response.should == [200, '<person><id>3</id><name>three</name></person>']
59
+ total_models.should == 3
60
+
61
+ get "/people/99"
62
+ normalized_response.should == [404, 'route not found']
63
+ total_models.should == 3
64
+ end
65
+
66
+ it 'should update a person' do
67
+ get '/people/2'
68
+ normalized_response.should == [200, '<person><id>2</id><name>two</name></person>']
69
+ total_models.should == 3
70
+
71
+ put '/people/2', {:name => 'tomorrow'}
72
+ normalized_response.should == [302, 'person updated']
73
+ total_models.should == 3
74
+
75
+ get '/people/2'
76
+ normalized_response.should == [200, '<person><id>2</id><name>tomorrow</name></person>']
77
+ total_models.should == 3
78
+ end
79
+
80
+ it 'should destroy a person' do
81
+ get '/people'
82
+ normalized_response.should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
83
+ total_models.should == 3
84
+
85
+ delete '/people/2'
86
+ normalized_response.should == [302, 'person destroyed']
87
+ total_models.should == 2
88
+
89
+ get '/people'
90
+ normalized_response.should == [200, '<people><person><id>1</id></person><person><id>3</id></person></people>']
91
+ total_models.should == 2
92
+
93
+ get '/people/2'
94
+ normalized_response.should == [404, 'route not found']
95
+ total_models.should == 2
96
+ end
97
+
98
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,160 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'spec/interop/test'
4
+ require 'rack/test'
5
+ require 'english/inflect'
6
+ require 'haml'
7
+ require 'dm-core'
8
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
9
+ require 'sinatra/base'
10
+ require 'sinatra/rest'
11
+
12
+ Sinatra::Base.set(:environment, :test)
13
+ Test::Unit::TestCase.send :include, Rack::Test::Methods
14
+
15
+ #
16
+ # Sets up a Sinatra::Base subclass defined with the block
17
+ # given. Used in setup or individual spec methods to establish
18
+ # the application.
19
+
20
+ def app
21
+ @app
22
+ end
23
+
24
+ def mock_app(&block)
25
+ base = Sinatra::Base
26
+ base.register Sinatra::REST
27
+ base.helpers Sinatra::REST::Helpers
28
+ @app = Sinatra.new(base) do
29
+ set :views, File.dirname(__FILE__) + '/views'
30
+ not_found do
31
+ 'route not found'
32
+ end
33
+ end
34
+ @app.instance_eval(&block) if block_given?
35
+ end
36
+
37
+ #
38
+ # sets rest in a sinatra instance
39
+ # and returns the block's result, if a block is given
40
+ def mock_rest(model, options={}, &block)
41
+ mock_app do
42
+ rest model, options
43
+ self.new.instance_eval do
44
+ self.instance_eval(&block) if block_given?
45
+ end
46
+ end
47
+ end
48
+
49
+ #
50
+ # normalize for easier testing
51
+ def normalized_response
52
+ return last_response.status, last_response.body.gsub(/>\s+</, '><').strip
53
+ end
54
+
55
+ # index GET /models
56
+ def index(url)
57
+ get url
58
+ normalized_response
59
+ end
60
+
61
+ # new GET /models/new
62
+ def new(url)
63
+ get url
64
+ normalized_response
65
+ end
66
+
67
+ # create POST /models
68
+ def create(url, params={})
69
+ post url, params
70
+ normalized_response
71
+ end
72
+
73
+ # show GET /models/1
74
+ def show(url)
75
+ get url
76
+ normalized_response
77
+ end
78
+
79
+ # edit GET /models/1/edit
80
+ def edit(url)
81
+ get url
82
+ normalized_response
83
+ end
84
+
85
+ # update PUT /models/1
86
+ def update(url, params={})
87
+ put url, params
88
+ normalized_response
89
+ end
90
+
91
+ # destroy DELETE /models/1
92
+ def destroy(url)
93
+ delete url
94
+ normalized_response
95
+ end
96
+
97
+
98
+ ##
99
+ ## kind of a 'minimal model'
100
+ class Person
101
+ attr_accessor :id
102
+ attr_accessor :name
103
+
104
+ def initialize(*args)
105
+ #puts "new #{args.inspect}"
106
+ if args.size == 0
107
+ @id = nil
108
+ @name = nil
109
+ elsif args.size == 2
110
+ @id = args[0].to_i
111
+ @name = args[1]
112
+ else args.size == 1
113
+ update_attributes(args[0])
114
+ end
115
+ end
116
+
117
+ def save
118
+ #puts "save #{@id}"
119
+ @@people << self
120
+ self.id = @@people.size
121
+ end
122
+
123
+ def update_attributes(hash)
124
+ #puts "update_attributes #{hash.inspect}"
125
+ unless hash.empty?
126
+ @id = hash['id'].to_i if hash.include?('id')
127
+ @name = hash['name'] if hash.include?('name')
128
+ end
129
+ end
130
+
131
+ def self.delete(id)
132
+ #puts "delete #{id}"
133
+ @@people.delete_if {|person| person.id == id.to_i}
134
+ end
135
+
136
+ @@people = []
137
+
138
+ def self.all(criteria={})
139
+ #puts 'all'
140
+ return @@people
141
+ end
142
+
143
+ def self.find_by_id(id)
144
+ #puts "find_by_id #{id}"
145
+ all.find {|f| f.id == id.to_i}
146
+ end
147
+
148
+ def self.clear!
149
+ @@people = []
150
+ end
151
+
152
+ def self.reset!
153
+ clear!
154
+ Person.new(1, 'one').save
155
+ Person.new(2, 'two').save
156
+ Person.new(3, 'three').save
157
+ end
158
+ end
159
+
160
+
@@ -0,0 +1,76 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ describe 'url helpers' do
4
+
5
+ it 'should generate the correct urls for the model' do
6
+ mock_rest Person do
7
+ person = Person.new(99, 'foo')
8
+ url_for_people_create.should == '/people'
9
+ url_for_people_destroy(person).should == '/people/99'
10
+ url_for_people_edit(person).should == '/people/99/edit'
11
+ url_for_people_index.should == '/people'
12
+ url_for_people_new.should == '/people/new'
13
+ url_for_people_show(person).should == '/people/99'
14
+ url_for_people_update(person).should == '/people/99'
15
+ end
16
+ end
17
+
18
+ it 'should add :all helpers' do
19
+ mock_rest(Person) { methods.grep(/^url_for_people_/).sort }.should == [
20
+ "url_for_people_create",
21
+ "url_for_people_destroy",
22
+ "url_for_people_edit",
23
+ "url_for_people_index",
24
+ "url_for_people_new",
25
+ "url_for_people_show",
26
+ "url_for_people_update",
27
+ ]
28
+ end
29
+
30
+ it 'should add :readable helpers' do
31
+ mock_rest(Person, :routes => :readable) { methods.grep(/^url_for_people_/).sort }.should == [
32
+ "url_for_people_index",
33
+ "url_for_people_show",
34
+ ]
35
+ end
36
+
37
+ it 'should add :writeable helpers' do
38
+ mock_rest(Person, :routes => :writeable) { methods.grep(/^url_for_people_/).sort }.should == [
39
+ "url_for_people_create",
40
+ "url_for_people_destroy",
41
+ "url_for_people_index",
42
+ "url_for_people_show",
43
+ "url_for_people_update",
44
+ ]
45
+ end
46
+
47
+ it 'should add :editable helpers' do
48
+ mock_rest(Person, :routes => :editable) { methods.grep(/^url_for_people_/).sort }.should == [
49
+ "url_for_people_create",
50
+ "url_for_people_destroy",
51
+ "url_for_people_edit",
52
+ "url_for_people_index",
53
+ "url_for_people_new",
54
+ "url_for_people_show",
55
+ "url_for_people_update",
56
+ ]
57
+ end
58
+
59
+ it 'should add helpers by name' do
60
+ mock_rest(Person, :routes => [:new, :create, :destroy]) { methods.grep(/^url_for_people_/).sort }.should == [
61
+ "url_for_people_create",
62
+ "url_for_people_destroy",
63
+ "url_for_people_new",
64
+ ]
65
+ end
66
+
67
+ it 'should add helpers by mixing aliases and names' do
68
+ mock_rest(Person, :routes => [:readable, :create, :destroy]) { methods.grep(/^url_for_people_/).sort }.should == [
69
+ "url_for_people_create",
70
+ "url_for_people_destroy",
71
+ "url_for_people_index",
72
+ "url_for_people_show",
73
+ ]
74
+ end
75
+
76
+ end
@@ -0,0 +1,29 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ describe 'model inflection' do
4
+
5
+ def conjugate(model)
6
+ mock_app {
7
+ include Sinatra::REST
8
+ conjugate(model)
9
+ }
10
+ end
11
+
12
+ it "should conjugate a simple model name" do
13
+ conjugate(Person).should == %w(Person person people)
14
+ end
15
+
16
+ it "should conjugate a String as model name" do
17
+ conjugate('Person').should == %w(Person person people)
18
+ end
19
+
20
+ it "should conjugate a model name in camel cases" do
21
+ conjugate('SomePerson').should == %w(SomePerson some_person some_people)
22
+ end
23
+
24
+ it "should conjugate a model name without module" do
25
+ conjugate('MyModule::ModulePerson').should == %w(ModulePerson module_person module_people)
26
+ end
27
+
28
+ end
29
+
@@ -0,0 +1,112 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ describe 'routes' do
4
+
5
+ before(:each) do
6
+ Person.reset!
7
+ end
8
+
9
+ describe 'one by one' do
10
+
11
+ before(:each) do
12
+ mock_rest Person
13
+ end
14
+
15
+ it 'should list all people on index by their id' do
16
+ index('/people').should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
17
+ end
18
+
19
+ it 'should prepare an empty item on new' do
20
+ new('/people/new').should == [200, '<person><id></id><name></name></person>']
21
+ end
22
+
23
+ it 'should create an item on post' do
24
+ create('/people', :name => 'new resource').should == [302, 'person created']
25
+ end
26
+
27
+ it 'should show an item on get' do
28
+ show('/people/1').should == [200, '<person><id>1</id><name>one</name></person>']
29
+ end
30
+
31
+ it 'should get the item for editing' do
32
+ edit('/people/1/edit').should == [200, '<person><id>1</id><name>one</name></person>']
33
+ end
34
+
35
+ it 'should update an item on put' do
36
+ update('/people/1', :name => 'another name').should == [302, 'person updated']
37
+ end
38
+
39
+ it 'should destroy an item on delete' do
40
+ destroy('/people/1').should == [302, 'person destroyed']
41
+ end
42
+
43
+ end
44
+
45
+ describe 'options' do
46
+
47
+ it 'should add :all routes' do
48
+ mock_rest Person
49
+
50
+ index('/people').should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
51
+ new('/people/new').should == [200, '<person><id></id><name></name></person>']
52
+ create('/people', :name => 'new person').should == [302, "person created"]
53
+ show('/people/1').should == [200, '<person><id>1</id><name>one</name></person>']
54
+ edit('/people/1/edit').should == [200, "<person><id>1</id><name>one</name></person>"]
55
+ update('/people/1', :name => 'new name').should == [302, "person updated"]
56
+ destroy('/people/1').should == [302, "person destroyed"]
57
+ end
58
+
59
+ it 'should add :readable routes' do
60
+ mock_rest Person, :routes => :readable
61
+
62
+ index('/people').should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
63
+ show('/people/1').should == [200, '<person><id>1</id><name>one</name></person>']
64
+
65
+ new('/people/new').should == [404, "route not found"]
66
+ create('/people', :name => 'new person').should == [404, "route not found"]
67
+ edit('/people/1/edit').should == [404, "route not found"]
68
+ update('/people/1', :name => 'new name').should == [404, "route not found"]
69
+ destroy('/people/1').should == [404, "route not found"]
70
+ end
71
+
72
+ it 'should add :writeable routes' do
73
+ mock_rest Person, :routes => :writeable
74
+
75
+ index('/people').should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
76
+ show('/people/1').should == [200, '<person><id>1</id><name>one</name></person>']
77
+ create('/people', :name => 'new person').should == [302, "person created"]
78
+ update('/people/1', :name => 'new name').should == [302, "person updated"]
79
+ destroy('/people/1').should == [302, "person destroyed"]
80
+
81
+ new('/people/new').should == [404, "route not found"]
82
+ edit('/people/1/edit').should == [404, "route not found"]
83
+ end
84
+
85
+ it 'should add :editable routes' do
86
+ mock_rest Person, :routes => :editable
87
+
88
+ index('/people').should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
89
+ new('/people/new').should == [200, '<person><id></id><name></name></person>']
90
+ create('/people', :name => 'new person').should == [302, "person created"]
91
+ show('/people/1').should == [200, '<person><id>1</id><name>one</name></person>']
92
+ edit('/people/1/edit').should == [200, "<person><id>1</id><name>one</name></person>"]
93
+ update('/people/1', :name => 'new name').should == [302, "person updated"]
94
+ destroy('/people/1').should == [302, "person destroyed"]
95
+ end
96
+
97
+ it 'should add routes by name' do
98
+ mock_rest Person, :routes => [:readable, :new, :create]
99
+
100
+ index('/people').should == [200, '<people><person><id>1</id></person><person><id>2</id></person><person><id>3</id></person></people>']
101
+ show('/people/1').should == [200, '<person><id>1</id><name>one</name></person>']
102
+ new('/people/new').should == [200, '<person><id></id><name></name></person>']
103
+ create('/people', :name => 'new person').should == [302, "person created"]
104
+
105
+ edit('/people/1/edit').should == [404, "route not found"]
106
+ update('/people/1', :name => 'new name').should == [404, "route not found"]
107
+ destroy('/people/1').should == [404, "route not found"]
108
+ end
109
+
110
+ end
111
+
112
+ end
data/test/test_spec.rb ADDED
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ describe 'test helpers' do
4
+
5
+ it 'should work with mock_app' do
6
+ Person.clear!
7
+ mock_app {
8
+ rest Person
9
+ }
10
+ index('/people').should == [200, '<people></people>']
11
+ end
12
+
13
+ it 'should work with mock_rest' do
14
+ Person.clear!
15
+ mock_rest Person
16
+ index('/people').should == [200, '<people></people>']
17
+ end
18
+
19
+ end
@@ -0,0 +1,4 @@
1
+ %person
2
+ %id= @person.id
3
+ %name= @person.name
4
+
@@ -0,0 +1,7 @@
1
+ %people
2
+ - @people.each do |person|
3
+ %person
4
+ %id= person.id
5
+
6
+
7
+
@@ -0,0 +1,4 @@
1
+ %person
2
+ %id= @person.id
3
+ %name= @person.name
4
+
@@ -0,0 +1,4 @@
1
+ %person
2
+ %id= @person.id
3
+ %name= @person.name
4
+
@@ -0,0 +1,4 @@
1
+ %word
2
+ %id= @word.id
3
+ %name= @word.name
4
+
@@ -0,0 +1,7 @@
1
+ %words
2
+ - @words.each do |word|
3
+ %word
4
+ %id= word.id
5
+
6
+
7
+
@@ -0,0 +1,4 @@
1
+ %word
2
+ %id= @word.id
3
+ %name= @word.name
4
+
@@ -0,0 +1,4 @@
1
+ %word
2
+ %id= @word.id
3
+ %name= @word.name
4
+
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mikeycgto-sinatra-rest
3
+ version: !ruby/object:Gem::Version
4
+ hash: 13
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 4
9
+ - 1
10
+ version: 0.4.1
11
+ platform: ruby
12
+ authors:
13
+ - blindgaenger
14
+ - kyledrake
15
+ - dominiquebrezinski
16
+ - mikeycgto
17
+ autorequire:
18
+ bindir: bin
19
+ cert_chain: []
20
+
21
+ date: 2010-06-08 00:00:00 -04:00
22
+ default_executable:
23
+ dependencies:
24
+ - !ruby/object:Gem::Dependency
25
+ name: sinatra
26
+ prerelease: false
27
+ requirement: &id001 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ hash: 57
33
+ segments:
34
+ - 0
35
+ - 9
36
+ - 1
37
+ version: 0.9.1
38
+ type: :runtime
39
+ version_requirements: *id001
40
+ - !ruby/object:Gem::Dependency
41
+ name: english
42
+ prerelease: false
43
+ requirement: &id002 !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ hash: 11
49
+ segments:
50
+ - 0
51
+ - 5
52
+ - 0
53
+ version: 0.5.0
54
+ type: :runtime
55
+ version_requirements: *id002
56
+ description:
57
+ email: blindgaenger@gmail.com
58
+ executables: []
59
+
60
+ extensions: []
61
+
62
+ extra_rdoc_files: []
63
+
64
+ files:
65
+ - Rakefile
66
+ - README.textile
67
+ - lib/sinatra/rest.rb
68
+ - lib/sinatra/rest/adapters.rb
69
+ - lib/sinatra/rest/rest.yaml
70
+ - test/adapters_spec.rb
71
+ - test/call_order_spec.rb
72
+ - test/crud_spec.rb
73
+ - test/helper.rb
74
+ - test/helpers_spec.rb
75
+ - test/inflection_spec.rb
76
+ - test/routes_spec.rb
77
+ - test/test_spec.rb
78
+ - test/views/people/edit.haml
79
+ - test/views/people/index.haml
80
+ - test/views/people/new.haml
81
+ - test/views/people/show.haml
82
+ - test/views/words/edit.haml
83
+ - test/views/words/index.haml
84
+ - test/views/words/new.haml
85
+ - test/views/words/show.haml
86
+ has_rdoc: true
87
+ homepage: http://github.com/mikeycgto/sinatra-rest
88
+ licenses: []
89
+
90
+ post_install_message:
91
+ rdoc_options: []
92
+
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ requirements: []
114
+
115
+ rubyforge_project:
116
+ rubygems_version: 1.3.7
117
+ signing_key:
118
+ specification_version: 3
119
+ summary: Generates RESTful routes for the models of a Sinatra application (ActiveRecord, DataMapper, Stone)
120
+ test_files: []
121
+