rack-backend-api 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +14 -14
- data/lib/backend_api.rb +2 -2
- data/lib/sequel_rack_backend_api_adapter.rb +3 -0
- data/rack-backend-api.gemspec +1 -1
- data/test/db.rb +1 -0
- data/test/spec_backend_api.rb +45 -1
- data/test/spec_sequel_adapter.rb +6 -0
- metadata +4 -4
data/README.md
CHANGED
@@ -7,13 +7,13 @@ while only concentrating on the interface.
|
|
7
7
|
All the database interactions are handled by the API.
|
8
8
|
|
9
9
|
The project is made with a Rack middleware and a small adapter for the Sequel ORM.
|
10
|
-
One of the chapter explains how to create an
|
10
|
+
One of the chapter explains how to create an adapter for another ORM (if you do one, please share).
|
11
11
|
|
12
12
|
Also this tool is part of a toolkit that is made for creating a CMS (in a modular way).
|
13
13
|
Here are the others:
|
14
14
|
|
15
15
|
- [Crushyform](https://github.com/mig-hub/sequel-crushyform): A Sequel plugin for building forms in a painless way and as flexible as possible.
|
16
|
-
- [Stash Magic](https://github.com/mig-hub/stash_magic): A simple attachment system that also handles thumbnails or other styles via ImageMagick.
|
16
|
+
- [Stash Magic](https://github.com/mig-hub/stash_magic): A simple attachment system that also handles thumbnails or other styles via ImageMagick. Originally tested on Sequel ORM but purposedly easy to plug to something else.
|
17
17
|
- [Cerberus](https://github.com/mig-hub/cerberus): A Rack middleware for form-based authentication.
|
18
18
|
|
19
19
|
This project is still at an early stage so don't hesitate to ask any question if the documentation lacks something.
|
@@ -42,8 +42,8 @@ HOW TO USE IT
|
|
42
42
|
=============
|
43
43
|
|
44
44
|
BackendAPI is a Rack middleware that you have to put before your actual backend/CMS,
|
45
|
-
and
|
46
|
-
And it takes care of everything involving
|
45
|
+
and generally after an authentication middleware.
|
46
|
+
And it takes care of everything involving interaction with your database.
|
47
47
|
|
48
48
|
In reality, it does not HAVE to be with the Backend but it makes sense and that way,
|
49
49
|
both share the authentication middleware.
|
@@ -59,11 +59,11 @@ A rackup stack for your application might look like this:
|
|
59
59
|
[username, password] == ['username', 'password']
|
60
60
|
end
|
61
61
|
use BackendAPI
|
62
|
-
run Backend
|
62
|
+
run Backend.new
|
63
63
|
end
|
64
64
|
|
65
|
-
Your backend receives every request that the Restful API doesn't
|
66
|
-
The BackendAPI
|
65
|
+
Your backend receives every request that the Restful API doesn't recognise.
|
66
|
+
The BackendAPI recognises requests following this scheme:
|
67
67
|
|
68
68
|
METHOD /Backend-path/model_class/ID
|
69
69
|
|
@@ -80,7 +80,7 @@ Then if you need to delete the entry with ID 4:
|
|
80
80
|
|
81
81
|
DELETE /admin/blog_post/4
|
82
82
|
|
83
|
-
The API also understands a
|
83
|
+
The API also understands a CamelCased class name:
|
84
84
|
|
85
85
|
DELETE /admin/BlogPost/4
|
86
86
|
|
@@ -93,7 +93,7 @@ and therefore use the right action and method for POST and PUT requests.
|
|
93
93
|
The problem sometimes with a Restful API is that in real life,
|
94
94
|
in spite of the fact that not every requests are GET or POST it is sometimes forced.
|
95
95
|
The href of a link is always a GET, and the method for a form is
|
96
|
-
|
96
|
+
overridden if it is not GET or POST.
|
97
97
|
|
98
98
|
This is why Rack has a very handy middleware called MethodOverride.
|
99
99
|
You don't have to `use` it because BackendAPI puts it on the stack for you.
|
@@ -180,7 +180,7 @@ it is better to have your Backend middleware before the API in the Rack stack:
|
|
180
180
|
[username, password] == ['username', 'password']
|
181
181
|
end
|
182
182
|
use Backend
|
183
|
-
run BackendAPI
|
183
|
+
run BackendAPI.new
|
184
184
|
end
|
185
185
|
|
186
186
|
Then what you do is that you make your Backend middleware aware that if the GET param
|
@@ -224,22 +224,22 @@ It is done automatically if the constant `Sequel` is defined (so you have to req
|
|
224
224
|
Here are the methods to implement, most of them are just aliases for having a single name:
|
225
225
|
|
226
226
|
- `Model::backend_get( id )` Should return a single database entry with the id provided
|
227
|
-
- `Model::backend_post( hash-of-values )`
|
227
|
+
- `Model::backend_post( hash-of-values )` Generally equivalent to Model::new, it creates a new entry with provided values and without validating or saving
|
228
228
|
- `Model#backend_delete` Instance method that destroys the entry
|
229
|
-
- `Model#backend_put( hash-of-values )`
|
229
|
+
- `Model#backend_put( hash-of-values )` Generally equivalent to Model::update, it updates an existing entry with provided values and without validating or saving
|
230
230
|
|
231
231
|
Others are slightly more sophisticated:
|
232
232
|
|
233
233
|
- `Model#backend_save?` Returns true if the entry is validated and saved. It generally triggers the error messages for the form as well.
|
234
234
|
- `Model#default_backend_columns` This the list of columns in the forms when the list of fields is not provided via `fields` option
|
235
235
|
- `Model#backend_form( action_url, columns=nil, options={} )` It is only the wrapping of the form without the actual fields. Try to implement it like the Sequel one.
|
236
|
-
- `Model#backend_fields( columns )` These are the actual fields. There is a default
|
236
|
+
- `Model#backend_fields( columns )` These are the actual fields. There is a default behaviour that basically puts a `textarea` for everything. That works in most cases but this is meant to be overridden for a better solution. We recommend [Crushyform](https://rubygems.org/gems/sequel-crushyform) for Sequel because we did it so we know it plays well with BackendAPI, and also because you don't have anything more to do. BackendAPI knows you have [Crushyform](https://rubygems.org/gems/sequel-crushyform) and use it to create the fields.
|
237
237
|
- `Model#backend_delete_form( action_url, options={})` Basically sugar for Model#backend_form but with an empty array for columns, and these options `{:submit_text=>'X', :method=>'DELETE'}` predefined which you can override. We've seen before that it is for creating DELETE forms.
|
238
238
|
|
239
239
|
THANX
|
240
240
|
=====
|
241
241
|
|
242
|
-
I'd like to thank [Manveru](https://github.com/manveru), [Pistos](https://github.com/pistos) and many others on the #ramaze IRC channel for being friendly, helpful and obviously
|
242
|
+
I'd like to thank [Manveru](https://github.com/manveru), [Pistos](https://github.com/pistos) and many others on the #ramaze IRC channel for being friendly, helpful and obviously savvy.
|
243
243
|
|
244
244
|
Also I'd like to thank [Konstantin Haase](https://github.com/rkh) for the same reasons as he helped me many times on #rack issues,
|
245
245
|
and because [almost-sinatra](https://github.com/rkh/almost-sinatra) is just made with the 8 nicest lines of code to read.
|
data/lib/backend_api.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class BackendAPI
|
2
|
-
VERSION = [0,0,
|
2
|
+
VERSION = [0,0,5]
|
3
3
|
WRAP = <<-EOT
|
4
4
|
<!doctype html>
|
5
5
|
<html>
|
@@ -102,7 +102,7 @@ class BackendAPI
|
|
102
102
|
@res.redirect(::Rack::Utils::unescape(@req['_destination']))
|
103
103
|
end
|
104
104
|
else
|
105
|
-
form = @model_instance.backend_form(@req.path, @req['model'].keys, :destination => @req['_destination'], :submit_text => @req['_submit_text'], :no_wrap => @req['_no_wrap'])
|
105
|
+
form = @model_instance.backend_form(@req.path, @req['fields']||@req['model'].keys, :destination => @req['_destination'], :submit_text => @req['_submit_text'], :no_wrap => @req['_no_wrap'])
|
106
106
|
@res.write(wrap_form(form))
|
107
107
|
@res.status=400 # Bad Request
|
108
108
|
end
|
@@ -28,6 +28,9 @@ module ::Sequel::Plugins::RackBackendApiAdapter
|
|
28
28
|
o << "<input type='hidden' name='_destination' value='#{opts[:destination]}' />\n" unless opts[:destination].nil?
|
29
29
|
o << "<input type='hidden' name='_submit_text' value='#{opts[:submit_text]}' />\n" unless opts[:submit_text].nil?
|
30
30
|
o << "<input type='hidden' name='_no_wrap' value='#{opts[:no_wrap]}' />\n" unless opts[:no_wrap].nil?
|
31
|
+
cols.each do |c|
|
32
|
+
o << "<input type='hidden' name='fields[]' value='#{c}' />\n"
|
33
|
+
end
|
31
34
|
o << "<input type='submit' name='save' value='#{opts[:submit_text] || 'SAVE'}' />\n"
|
32
35
|
o << "</form>\n"
|
33
36
|
o
|
data/rack-backend-api.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'rack-backend-api'
|
3
|
-
s.version = "0.0.
|
3
|
+
s.version = "0.0.5"
|
4
4
|
s.platform = Gem::Platform::RUBY
|
5
5
|
s.summary = "A Rack middleware that provides a simple API for your Admin section"
|
6
6
|
s.description = "The purpose of this Rack Middleware is to provide an API that interfaces with database actions in order to build a CMS."
|
data/test/db.rb
CHANGED
data/test/spec_backend_api.rb
CHANGED
@@ -22,7 +22,7 @@ def wrap(title, form) #mock wrapped versions of forms when not XHR
|
|
22
22
|
BackendAPI::WRAP % [title,form]
|
23
23
|
end
|
24
24
|
|
25
|
-
class WrappingMiddleware
|
25
|
+
class WrappingMiddleware # Wrap but with the previous middleware
|
26
26
|
def initialize(app); @app = app; end
|
27
27
|
def call(env)
|
28
28
|
if Rack::Request.new(env)['_no_wrap']
|
@@ -44,21 +44,25 @@ class WrappingMiddleware
|
|
44
44
|
end
|
45
45
|
|
46
46
|
describe 'API Misc' do
|
47
|
+
|
47
48
|
should "Send 404 X-cascade if no response at the bottom of the Rack stack - Builder::run" do
|
48
49
|
res = req_lint(BackendAPI.new).get('/zzz')
|
49
50
|
res.status.should==404
|
50
51
|
res.headers['X-Cascade'].should=='pass'
|
51
52
|
end
|
53
|
+
|
52
54
|
should 'Follow the Rack stack if response is not found - Builder::use' do
|
53
55
|
res = req_lint(BackendAPI.new(dummy_app)).get('/')
|
54
56
|
res.status.should==200
|
55
57
|
res.body.should=='dummy'
|
56
58
|
end
|
59
|
+
|
57
60
|
should "Have a special path for sending version" do
|
58
61
|
res = req_lint(BackendAPI.new(dummy_app)).get('/_version')
|
59
62
|
res.status.should==200
|
60
63
|
res.body.should==BackendAPI::VERSION.join('.')
|
61
64
|
end
|
65
|
+
|
62
66
|
should "Accept CamelCased or under_scrored class names" do
|
63
67
|
# I prefer CamelCased as it only needs to be eval(ed)
|
64
68
|
# But people are used to under_scrored
|
@@ -72,6 +76,7 @@ describe 'API Misc' do
|
|
72
76
|
end
|
73
77
|
|
74
78
|
describe 'API Post' do
|
79
|
+
|
75
80
|
should "Create a new entry in the database and send a 201 response" do
|
76
81
|
res = req_lint(BackendAPI.new).post('/haiku', :params => {'model' => {'title' => 'Summer', 'body' => "Summer was missing\nI cannot accept that\nI need to bake in the sun"}})
|
77
82
|
res.status.should==201 # Created
|
@@ -79,14 +84,17 @@ describe 'API Post' do
|
|
79
84
|
haiku.title.should=='Summer'
|
80
85
|
haiku.body.should=="Summer was missing\nI cannot accept that\nI need to bake in the sun"
|
81
86
|
end
|
87
|
+
|
82
88
|
should "Fallback to an update if there is an id provided" do
|
83
89
|
req_lint(BackendAPI.new).post('/haiku/4', :params => {'model' => {'title' => 'Summer is not new !!!'}})
|
84
90
|
Haiku.filter(:title => 'Summer is not new !!!').first.id.should==4
|
85
91
|
end
|
92
|
+
|
86
93
|
should "Accept a new entry with no attributes as long as it is valid" do
|
87
94
|
res = req_lint(BackendAPI.new).post('/haiku')
|
88
95
|
res.status.should==201
|
89
96
|
end
|
97
|
+
|
90
98
|
should "Send back the appropriate form when the creation is not valid" do
|
91
99
|
|
92
100
|
res = req_lint(BackendAPI.new).post('/haiku', :params => {'model' => {'title' => '13'}})
|
@@ -100,15 +108,27 @@ describe 'API Post' do
|
|
100
108
|
res.status.should==400
|
101
109
|
res.body.should==compared.backend_form('/haiku', ['title'])
|
102
110
|
end
|
111
|
+
|
112
|
+
should "Use fields instead of model keys when validation fails (if possible)" do
|
113
|
+
# That helps keeping the same order when validation doesn't pass
|
114
|
+
# Also it keeps fields not sent when untouched, like checkboxes or images
|
115
|
+
res = req_lint(BackendAPI.new).post('/haiku', :params => {'model' => {'title' => '13'}})
|
116
|
+
res.body.should.not.match(/value='body'/)
|
117
|
+
res = req_lint(BackendAPI.new).post('/haiku', :params => {'model' => {'title' => '13'}, 'fields' => ['title', 'body']})
|
118
|
+
res.body.should.match(/value='body'/)
|
119
|
+
end
|
120
|
+
|
103
121
|
should "Accept a destination for when Entry is validated and request is not XHR" do
|
104
122
|
res = req_lint(BackendAPI.new(dummy_app)).post('/haiku', :params => {'_destination' => 'http://www.domain.com/list.xml', 'model' => {'title' => 'Destination Summer'}})
|
105
123
|
res.status.should==302
|
106
124
|
res.headers['Location']=='http://www.domain.com/list.xml'
|
107
125
|
Haiku.order(:id).last.title.should=='Destination Summer'
|
108
126
|
end
|
127
|
+
|
109
128
|
should "Keep _destination until form is validated" do
|
110
129
|
req_lint(BackendAPI.new).post('/haiku', :params => {'_destination' => '/', 'model' => {'title' => '13'}}).body.should.match(/name='_destination'.*value='\/'/)
|
111
130
|
end
|
131
|
+
|
112
132
|
should "Keep _no_wrap until form is validated" do
|
113
133
|
compared = Haiku.new.set('title' => '13')
|
114
134
|
compared.valid?
|
@@ -119,21 +139,26 @@ describe 'API Post' do
|
|
119
139
|
end
|
120
140
|
|
121
141
|
describe 'API Get' do
|
142
|
+
|
122
143
|
should "Return the form for a fresh entry when no id is provided" do
|
123
144
|
req_lint(BackendAPI.new).get('/haiku').body.should==wrap('Haiku', Haiku.new.backend_form('/haiku'))
|
124
145
|
end
|
146
|
+
|
125
147
|
should "Return the form for an update when id is provided" do
|
126
148
|
req_lint(BackendAPI.new).get('/haiku/3').body.should==wrap('Haiku', Haiku[3].backend_form('/haiku/3'))
|
127
149
|
end
|
150
|
+
|
128
151
|
should "Be able to send a form with selected set of fields" do
|
129
152
|
req_lint(BackendAPI.new).get('/haiku', :params => {'fields' => ['title']}).body.should==wrap('Haiku', Haiku.new.backend_form('/haiku', ['title']))
|
130
153
|
req_lint(BackendAPI.new).get('/haiku/3', :params => {'fields' => ['title']}).body.should==wrap('Haiku', Haiku[3].backend_form('/haiku/3', ['title']))
|
131
154
|
end
|
155
|
+
|
132
156
|
should "Update the entry before building the form if model parameter is used" do
|
133
157
|
update = {'title' => 'Changed'}
|
134
158
|
req_lint(BackendAPI.new).get('/haiku', :params => {'model' => update}).body.should==wrap('Haiku', Haiku.new.set(update).backend_form('/haiku'))
|
135
159
|
req_lint(BackendAPI.new).get('/haiku/3', :params => {'model' => update}).body.should==wrap('Haiku', Haiku[3].set(update).backend_form('/haiku/3'))
|
136
160
|
end
|
161
|
+
|
137
162
|
should "Return a partial if the request is XHR or param _no_wrap is used" do
|
138
163
|
req_lint(BackendAPI.new).get('/haiku', "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest").body.should==Haiku.new.backend_form('/haiku')
|
139
164
|
req_lint(BackendAPI.new).get('/haiku?_no_wrap=true').body.should==Haiku.new.backend_form('/haiku', nil, {:no_wrap=>'true'})
|
@@ -142,6 +167,7 @@ describe 'API Get' do
|
|
142
167
|
end
|
143
168
|
|
144
169
|
describe 'API Put' do
|
170
|
+
|
145
171
|
should "Update a database entry that exists and send a 201 response" do
|
146
172
|
res = req_lint(BackendAPI.new).put('/haiku/3', :params => {'model' => {'body' => "Maybe I have no inspiration\nBut at least\nIt should be on three lines"}})
|
147
173
|
res.status.should==201 # Created
|
@@ -149,14 +175,17 @@ describe 'API Put' do
|
|
149
175
|
haiku.body.should=="Maybe I have no inspiration\nBut at least\nIt should be on three lines"
|
150
176
|
haiku.title.should=='Spring'
|
151
177
|
end
|
178
|
+
|
152
179
|
should "Work with MethodOverride" do
|
153
180
|
req_lint(BackendAPI.new).post('/haiku/3', :params => {'_method' => 'PUT', 'model' => {'title' => 'Spring Wow !!!'}})
|
154
181
|
Haiku[3].title.should=='Spring Wow !!!'
|
155
182
|
end
|
183
|
+
|
156
184
|
should "Not break if one updates with no changes" do
|
157
185
|
res = req_lint(BackendAPI.new).put('/haiku/3')
|
158
186
|
res.status.should==201
|
159
187
|
end
|
188
|
+
|
160
189
|
should "Send back the appropriate form when the creation is not valid" do
|
161
190
|
|
162
191
|
res = req_lint(BackendAPI.new).put('/haiku/3', :params => {'model' => {'title' => '13'}})
|
@@ -170,15 +199,27 @@ describe 'API Put' do
|
|
170
199
|
res.status.should==400
|
171
200
|
res.body.should==compared.backend_form('/haiku/3', ['title'])
|
172
201
|
end
|
202
|
+
|
203
|
+
should "Use fields instead of model keys when validation fails (if possible)" do
|
204
|
+
# That helps keeping the same order when validation doesn't pass
|
205
|
+
# Also it keeps fields not sent when untouched, like checkboxes or images
|
206
|
+
res = req_lint(BackendAPI.new).put('/haiku/3', :params => {'model' => {'title' => '13'}})
|
207
|
+
res.body.should.not.match(/value='body'/)
|
208
|
+
res = req_lint(BackendAPI.new).put('/haiku/3', :params => {'model' => {'title' => '13'}, 'fields' => ['title', 'body']})
|
209
|
+
res.body.should.match(/value='body'/)
|
210
|
+
end
|
211
|
+
|
173
212
|
should "Accept a destination for when Update is validated and request is not XHR" do
|
174
213
|
res = req_lint(BackendAPI.new(dummy_app)).post('/haiku/3', :params => {'_method' => 'PUT', '_destination' => '/', 'model' => {'title' => 'Spring destination !!!'}})
|
175
214
|
res.status.should==302
|
176
215
|
res.headers['Location']=='/'
|
177
216
|
Haiku[3].title.should=='Spring destination !!!'
|
178
217
|
end
|
218
|
+
|
179
219
|
should "keep destination until form is validated" do
|
180
220
|
req_lint(BackendAPI.new).put('/haiku/3', :params => {'_destination' => '/', 'model' => {'title' => '13'}}).body.should.match(/name='_destination'.*value='\/'/)
|
181
221
|
end
|
222
|
+
|
182
223
|
should "Keep _no_wrap until form is validated" do
|
183
224
|
compared = Haiku[3].set('title' => '13')
|
184
225
|
compared.valid?
|
@@ -189,15 +230,18 @@ describe 'API Put' do
|
|
189
230
|
end
|
190
231
|
|
191
232
|
describe 'API Delete' do
|
233
|
+
|
192
234
|
should "Delete a database entry that exists and send a 204 response" do
|
193
235
|
res = req_lint(BackendAPI.new).delete('/haiku/1')
|
194
236
|
res.status.should==204 # No Content
|
195
237
|
Haiku[1].should==nil
|
196
238
|
end
|
239
|
+
|
197
240
|
should "Work with MethodOverride" do
|
198
241
|
req_lint(BackendAPI.new(dummy_app)).post('/haiku/2', :params => {'_method' => 'DELETE'})
|
199
242
|
Haiku[2].should==nil
|
200
243
|
end
|
244
|
+
|
201
245
|
should "Accept a destination" do
|
202
246
|
res = req_lint(BackendAPI.new).delete('/haiku/3', :params => {'_destination' => '/'})
|
203
247
|
res.status.should==302
|
data/test/spec_sequel_adapter.rb
CHANGED
@@ -65,6 +65,12 @@ describe 'Sequel Adapter' do
|
|
65
65
|
f.should.match(/name='_submit_text' value='CREATE'/)
|
66
66
|
end
|
67
67
|
|
68
|
+
should 'Send the list of fields in the correct order' do
|
69
|
+
# That helps keeping the same order when validation doesn't pass
|
70
|
+
# Also it keeps fields not sent when untouched, like checkboxes or images
|
71
|
+
Pic.new.backend_form('/url').scan(/name='fields\[\]'/).size.should==2
|
72
|
+
end
|
73
|
+
|
68
74
|
should 'Have a backend_delete_form method - pure HTTP way of deleting records with HTTP DELETE method' do
|
69
75
|
form = Haiku.first.backend_delete_form('/url')
|
70
76
|
form.should.match(/name='_method' value='DELETE'/)
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-backend-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 21
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 5
|
10
|
+
version: 0.0.5
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Mickael Riga
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-07-
|
18
|
+
date: 2011-07-22 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|