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 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 adpater for another ORM (if you do one, please share).
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. Originaly tested on Sequel ORM but purposedly easy to plug to something else.
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 generaly after an authentication middleware.
46
- And it takes care of everything involving interraction with your database.
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 recognize.
66
- The BackendAPI recognizes requests following this scheme:
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 camelcased class name:
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
- overriden if it is not GET or POST.
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 )` Generaly equivalent to Model::new, it creates a new entry with provided values and without validating or saving
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 )` Generaly equivalent to Model::update, it updates an existing entry with provided values and without validating or saving
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 behavior that basically puts a textarea for everything. That works in most cases but this is meant to be overriden for a better solution. We recommand [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.
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 savy.
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,4]
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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'rack-backend-api'
3
- s.version = "0.0.4"
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
@@ -35,6 +35,7 @@ end
35
35
  class Pic < ::Sequel::Model
36
36
  set_schema do
37
37
  primary_key :id
38
+ String :name
38
39
  String :image, :crushyform=>{:type=>:attachment}
39
40
  end
40
41
  create_table unless table_exists?
@@ -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
@@ -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: 23
4
+ hash: 21
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 4
10
- version: 0.0.4
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-21 00:00:00 +01:00
18
+ date: 2011-07-22 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies: []
21
21