rack-backend-api 0.0.4 → 0.0.5
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.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
|
|