rack-backend-api 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Mickael Riga
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,202 @@
1
+ BACKEND API
2
+ ===========
3
+
4
+ The inspiration for this Rack middleware is the CouchDB API.
5
+ We wanted to create a middleware that provides every URLs you need in order to build a CMS
6
+ while only concentrating on the interface.
7
+ All the database interactions are handled by the API.
8
+
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).
11
+
12
+ Also this tool is part of a toolkit that is made for creating a CMS (in a modular way).
13
+ Here are the others:
14
+
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.
17
+ - [Cerberus](https://github.com/mig-hub/cerberus): A Rack middleware for form-based authentication.
18
+
19
+ This project is still at an early stage so don't hesitate to ask any question if the documentation lacks something.
20
+ And a good way to get started is to try the example in the example folder of this library.
21
+ It is a very basic (but complete) admin system.
22
+ A lot better if you have [Crushyform](https://rubygems.org/gems/sequel-crushyform) installed (nothing more to do except having it installed).
23
+
24
+ Once you are in the root directory of the library, you can start it with:
25
+
26
+ rackup example/config.ru
27
+
28
+ The file `basic_admin.rb` contains the Backend itself and shows how to use the URLs of the API.
29
+
30
+ You can use it for any kind of Rack application (Sinatra, Ramaze, Merb, Rails...).
31
+ Ramaze/Innate is not the most obvious to use as a middleware, but this is the one I use the most,
32
+ so drop me a line if you don't know how to do.
33
+
34
+ HOW TO INSTALL
35
+ ==============
36
+
37
+ This is a Gem so you can install it with:
38
+
39
+ sudo gem install rack-backend-api
40
+
41
+ HOW TO USE IT
42
+ =============
43
+
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.
47
+
48
+ In reality, it does not HAVE to be with the Backend but it makes sense and that way,
49
+ both share the authentication middleware.
50
+
51
+ A rackup stack for your application might look like this:
52
+
53
+ map '/' do
54
+ run Frontend
55
+ end
56
+
57
+ map '/admin' do
58
+ use Rack::Auth::Basic, "your-realm" do |username, password|
59
+ [username, password] == ['username', 'password']
60
+ end
61
+ use BackendAPI
62
+ run Backend
63
+ end
64
+
65
+ Your backend receives every request that the Restful API doesn't recognize.
66
+ The BackendAPI recognizes requests following this scheme:
67
+
68
+ METHOD /Backend-path/model_class/ID
69
+
70
+ The ID is not always relevant.
71
+ So if you have a model class called BlogPost and you want to get the form for the entry with ID 4:
72
+
73
+ GET /admin/blog_post/4
74
+
75
+ If you don't put an ID, it means you want the form for a brand new entry:
76
+
77
+ GET /admin/blog_post
78
+
79
+ Then if you need to delete the entry with ID 4:
80
+
81
+ DELETE /admin/blog_post/4
82
+
83
+ To be honest, that is almost everything you need because the ORM adapter builds the forms
84
+ and therefore use the right action and method for POST and PUT requests.
85
+
86
+ The problem sometimes with a Restful API is that in real life,
87
+ in spite of the fact that not every requests are GET or POST it is sometimes forced.
88
+ The href of a link is always a GET, and the method for a form is
89
+ overriden if it is not GET or POST.
90
+
91
+ This is why Rack has a very handy middleware called MethodOverride.
92
+ You don't have to `use` it because BackendAPI puts it on the stack for you.
93
+ Basically when you have it, you can send the method you really wanted in the POSTed parameter called "_method",
94
+ and the middleware override the method for you.
95
+ This is how the adapter makes forms with PUT requests.
96
+
97
+ But unfortunately you can only use MethodOverride on POST requests,
98
+ but you might want to have it on links.
99
+
100
+ Here is a concrete example:
101
+ You want to put in your CMS a link for deleting blog post.
102
+ But a link is going to be a GET request.
103
+ Of course you could use Ajax and anyway you probably will,
104
+ but it is a good practice to make it possible without javascript.
105
+ So your link could look like this:
106
+
107
+ <a href="/admin/blog_post/4?_method=DELETE"> X </a>
108
+
109
+ But it doesn't work because links are GET requests.
110
+ Fortunately this is a common task so there is a method that makes DELETE buttons available as a form:
111
+
112
+ @blog_post.backend_delete_form("/admin/blog_post/4", { :destination => "/admin/list/blog_post" })
113
+
114
+ The `:destination` is where you go when the job is done.
115
+ You also can change the option `:submit_text` which is what the button says.
116
+ By default, the DELETE form button says "X".
117
+
118
+ The `:destination` option is also in the API as `_destination`.
119
+ Use it in order to specify where to go when the entry is validated.
120
+ Because before it is validated you'll get the form again with error messages.
121
+
122
+ Say we need a link for creating a blog post, and then when validated, we want to go back to the list page:
123
+
124
+ <a href="/admin/blog_post?_destination=%2Fadmin%2Flist%2Fblog_post"> Create new Blog Post </a>
125
+
126
+ Of course, the page `/admin/list/blog_post` is a page of your Backend/CMS.
127
+ The form will be POSTed because there is no ID, which means it is a new entry.
128
+ On that list page, you could have a list of your posts with an "Edit" link:
129
+
130
+ My Interesting Post Number 4 - <a href="/admin/blog_post/4?_destination=%2Fadmin%2Flist%2Fblog_post"> Edit </a>
131
+
132
+ You also have another option called `fields` which allows you to say which fields you want in that form.
133
+ The purpose of that is mainly to be able to edit a single value at a time:
134
+
135
+ Title: My Super Blog - <a href="/admin/blog_post/4?fields[]=title&_destination=%2Fadmin%2Flist%2Fblog_post"> Edit </a>
136
+
137
+ This will make a link to a form for editing the title of that Blog Post.
138
+ Please note that the option `fields` is an array.
139
+
140
+ Also don't forget to escape the URI like in the examples above.
141
+ You can do that with Rack::Utils :
142
+
143
+ ::Rack::Utils.escape "/admin/list/blog_post"
144
+
145
+ The option `:submit_text` is also available through the API as `_submit_text`.
146
+ It says "SAVE" by default but you might want it to say "CREATE" and "UPDATE" in appropriate cases,
147
+ like we did in the example.
148
+
149
+ A LITTLE BIT OF JAVASCRIPT
150
+ ==========================
151
+
152
+ This is a very crude way of dealing with forms I have to say.
153
+ And it is only like that for having unobtrusive javascript.
154
+ When you use the BackendAPI, your CMS is just about nice tricks,
155
+ nice interface using Ajax and getting the best of what the API has to offer.
156
+
157
+ The forms are really meant to be requested via an XHR,
158
+ so that you have access to style and javascript widgets like a date-picker for example.
159
+
160
+ HOW TO PLUG AN ORM
161
+ ==================
162
+
163
+ For the moment the only adapter available is for the Sequel ORM,
164
+ but it should be fairly easy to create one for any other ORM.
165
+ If you do one for DataMapper or whatever, please consider contributing
166
+ because it would make BackendAPI more interesting.
167
+
168
+ The adapter for Sequel is a regular plugin, but you don't have to declare it.
169
+ It is done automatically if the constant `Sequel` is defined.
170
+
171
+ Here are the methods to implement, most of them are just aliases for having a single name:
172
+
173
+ - `Model::backend_get( id )` Should return a single database entry with the id provided
174
+ - `Model::backend_post( hash-of-values )` Generaly equivalent to Model::new, it creates a new entry with provided values and without validating or saving
175
+ - `Model#backend_delete` Instance method that destroys the entry
176
+ - `Model#backend_put( hash-of-values )` Generaly equivalent to Model::update, it updates an existing entry with provided values and without validating or saving
177
+
178
+ Others are slightly more sophisticated:
179
+
180
+ - `Model#backend_save?` Returns true if the entry is validated and saved. It generally triggers the error messages for the form as well.
181
+ - `Model#default_backend_columns` This the list of columns in the forms when the list of fields is not provided via `fields` option
182
+ - `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.
183
+ - `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.
184
+ - `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.
185
+
186
+ THANX
187
+ =====
188
+
189
+ 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.
190
+
191
+ Also I'd like to thank [Konstantine Hasse](https://github.com/rkh) for the same reasons as he helped me many times on #rack issues,
192
+ and because [almost-sinatra](https://github.com/rkh/almost-sinatra) is just made with the 8 nicest lines of code to read.
193
+
194
+ CHANGE LOG
195
+ ==========
196
+
197
+ 0.0.1 First version
198
+
199
+ COPYRIGHT
200
+ =========
201
+
202
+ (c) 2011 Mickael Riga - see file LICENSE for details
@@ -0,0 +1,31 @@
1
+ BASIC_ADMIN = proc{
2
+
3
+ map '/' do
4
+ run proc{|env|
5
+ title = "<h1>Main Menu</h1>\n"
6
+ menu = env['basic_admin.models'].map do |m|
7
+ "<p><a href='%s/list/%s'>%s Management</a></p>\n" % [env['SCRIPT_NAME'],m,m]
8
+ end.join
9
+ [200,{'Content-Type'=>'text/html'},[title,menu]]
10
+ }
11
+ end
12
+ map '/list' do
13
+ run proc{|env|
14
+ path = env['SCRIPT_NAME']+env['PATH_INFO']
15
+ escaped_path = ::Rack::Utils::escape path
16
+ root_path = env['SCRIPT_NAME'].slice(0,env['SCRIPT_NAME'].rindex('/'))
17
+ model_name = env['PATH_INFO'][1..-1]
18
+ api_model_path = "%s/%s" % [root_path,model_name]
19
+ title = "<h1>%s List</h1>\n" % [model_name]
20
+ create_link = "<p><a href='%s?_destination=%s&_submit_text=CREATE'>Create %s</a></p>\n" % [api_model_path,escaped_path,model_name]
21
+ list = eval(model_name).all.map do |m|
22
+ api_inst_path = "%s/%s" % [api_model_path,m.id]
23
+ link = "<a href='%s?_destination=%s&_submit_text=UPDATE'>%s</a>\n" % [api_inst_path,escaped_path,m.to_label]
24
+ delete_form = m.backend_delete_form(api_inst_path, {:destination=>path})
25
+ link+delete_form
26
+ end.join
27
+ [200,{'Content-Type'=>'text/html'},[title,create_link,list]]
28
+ }
29
+ end
30
+
31
+ }
data/example/config.ru ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env rackup
2
+
3
+ ::Dir.chdir(::File.dirname(__FILE__)+'/..')
4
+ $:.unshift './lib'
5
+ require 'rubygems'
6
+ require 'test/db'
7
+ require 'backend_api'
8
+ require 'example/basic_admin'
9
+
10
+ use ::Rack::ContentLength
11
+ map '/' do
12
+ run proc{ |env|
13
+ [200,{'Content-Type'=>'text/html'}, ["Here is the Front-end. Real action is in: <a href='/admin'>/admin</a>."]]
14
+ }
15
+ end
16
+
17
+ map '/admin' do
18
+ use ::Rack::Config do |env|
19
+ env['basic_admin.models'] = [:Author, :Haiku]
20
+ end
21
+ use BackendAPI
22
+ run ::Rack::Builder.app(&BASIC_ADMIN)
23
+ end
@@ -0,0 +1,120 @@
1
+ class BackendAPI
2
+ VERSION = [0,0,1]
3
+ WRAP = <<-EOT
4
+ <!doctype html>
5
+ <html>
6
+ <head><meta charset="UTF-8" /><title>%s</title></head>
7
+ <body>
8
+ %s
9
+ </body>
10
+ </html>
11
+ EOT
12
+
13
+ # Automatically use MethodOverride before
14
+ # Thx Konstantin Haase for the trick
15
+ def self.new(*); ::Rack::MethodOverride.new(super); end
16
+ def initialize(app=nil); @app = app; end
17
+ def call(env); dup.call!(env); end
18
+
19
+ def call!(env)
20
+ @req = ::Rack::Request.new(env)
21
+ @res = ::Rack::Response.new
22
+
23
+ # Simple dispatcher
24
+ @model_name, @id, *a = @req.path_info.split('/').find_all{|s| s!=''}
25
+
26
+ # Special case
27
+ return @res.finish{@res.write(v)} if @model_name=='_version'
28
+
29
+ build_model_vars
30
+ __send__(@req.request_method.downcase) unless @res.status==404
31
+
32
+ @res.status==404&&!@app.nil? ? @app.call(env) : @res.finish
33
+ end
34
+
35
+ private
36
+
37
+ # =========
38
+ # = Paths =
39
+ # =========
40
+
41
+ def v; VERSION.join('.'); end
42
+
43
+ # Create
44
+ def post
45
+ return put unless @id.nil?
46
+ @model_instance = @model_class.backend_post(@req['model'])
47
+ save_and_respond
48
+ end
49
+
50
+ # Read
51
+ def get
52
+ @model_instance ||= @model_class.backend_post
53
+ @model_instance.backend_put @req['model']
54
+ form = @model_instance.backend_form(@req.path, @req['fields'], :destination => @req['_destination'], :submit_text => @req['_submit_text'] )
55
+ @res.write(wrap_form(form))
56
+ end
57
+
58
+ # Update
59
+ def put
60
+ @model_instance.backend_put @req['model']
61
+ save_and_respond
62
+ end
63
+
64
+ # Delete
65
+ def delete
66
+ @model_instance.backend_delete
67
+ @req['_destination'].nil? ? @res.status=204 : @res.redirect(::Rack::Utils::unescape(@req['_destination'])) # 204 No Content
68
+ end
69
+
70
+ # Cost less than checking if is not GET, POST, PUT or DELETE
71
+ def head; get; end; def options; get; end; def patch; get; end; def trace; get; end
72
+
73
+ # ===========
74
+ # = Helpers =
75
+ # ===========
76
+
77
+ def build_model_vars
78
+ @model_class_name = camel_case(@model_name)
79
+ if !@model_name.nil? && ::Object.const_defined?(@model_class_name)
80
+ @model_class = eval(@model_class_name)
81
+ @model_instance = @model_class.backend_get(@id.to_i) unless @id.nil?
82
+ @req['model'] ||= {}
83
+ else
84
+ @res.status=404 # Not Found
85
+ @res.headers['X-Cascade']='pass'
86
+ @res.write 'Not Found'
87
+ end
88
+ end
89
+
90
+ def camel_case(s)
91
+ s.to_s.split('_').map{|e|e.capitalize}.join
92
+ end
93
+
94
+ def save_and_respond
95
+ if @model_instance.backend_save?
96
+ if @req['_destination'].nil?
97
+ @res.write 'OK'
98
+ @res.status=201 # Created
99
+ else
100
+ @res.redirect(::Rack::Utils::unescape(@req['_destination']))
101
+ end
102
+ else
103
+ form = @model_instance.backend_form(@req.path, @req['model'].keys, :destination => @req['_destination'], :submit_text => @req['_submit_text'])
104
+ @res.write(wrap_form(form))
105
+ @res.status=400 # Bad Request
106
+ end
107
+ end
108
+
109
+ def wrap_form(form)
110
+ if @req.xhr?
111
+ form
112
+ else
113
+ WRAP % [@model_class_name, form]
114
+ end
115
+ end
116
+
117
+ end
118
+
119
+ # Require Adapter when known ORM detected
120
+ ::Sequel::Model.plugin :rack_backend_api_adapter if defined? Sequel
@@ -0,0 +1,56 @@
1
+ module ::Sequel::Plugins::RackBackendApiAdapter
2
+
3
+ module ClassMethods
4
+ end
5
+
6
+ module InstanceMethods
7
+
8
+ def self.included(model_class)
9
+ model_class.class_eval do
10
+ class << self
11
+ alias backend_get []
12
+ alias backend_post new
13
+ end
14
+ alias backend_delete destroy
15
+ alias backend_put set
16
+ end
17
+ end
18
+
19
+ def backend_save?; valid? && save; end
20
+
21
+ def backend_form(url, cols=nil, opts={})
22
+ cols ||= default_backend_columns
23
+ fields_list = respond_to?(:crushyform) ? crushyform(cols) : backend_fields(cols)
24
+ o = "<form action='#{url}' method='POST' #{"enctype='multipart/form-data'" if fields_list.match(/type='file'/)} class='backend-form'>\n"
25
+ o << fields_list
26
+ method = self.new? ? 'POST' : 'PUT'
27
+ o << "<input type='hidden' name='_method' value='#{opts[:method] || method}' />\n"
28
+ o << "<input type='hidden' name='_destination' value='#{opts[:destination]}' />\n" unless opts[:destination].nil?
29
+ o << "<input type='hidden' name='_submit_text' value='#{opts[:submit_text]}' />\n" unless opts[:submit_text].nil?
30
+ o << "<input type='submit' name='save' value='#{opts[:submit_text] || 'SAVE'}' />\n"
31
+ o << "</form>\n"
32
+ o
33
+ end
34
+
35
+ def backend_delete_form(url, opts={}); backend_form(url, [], {:submit_text=>'X', :method=>'DELETE'}.update(opts)); end
36
+
37
+ # Silly but usable form prototype
38
+ # Not really meant to be used in a real case
39
+ # It uses a textarea for everything
40
+ # Override it
41
+ # Or even better, use Sequel-Crushyform plugin instead
42
+ def backend_fields(cols)
43
+ o = ''
44
+ cols.each do |c|
45
+ identifier = "#{id.to_i}-#{self.class}-#{c}"
46
+ o << "<label for='#{identifier}'>#{c.to_s.capitalize}</label><br />\n"
47
+ o << "<textarea id='#{identifier}' name='model[#{c}]'>#{self.send(c)}</textarea><br />\n"
48
+ end
49
+ end
50
+
51
+ # Can be overridden
52
+ def default_backend_columns; columns - [:id]; end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'rack-backend-api'
3
+ s.version = "0.0.1"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = "A Rack middleware that provides a simple API for your Admin section"
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."
7
+ s.files = `git ls-files`.split("\n").sort
8
+ s.require_path = './lib'
9
+ s.author = "Mickael Riga"
10
+ s.email = "mig@mypeplum.com"
11
+ s.homepage = "http://github.com/mig-hub/backend-api"
12
+ end
data/test/db.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'sequel'
2
+ ::Sequel::Model.plugin :schema
3
+ ::Sequel::Model.plugin :crushyform rescue nil
4
+ DB = ::Sequel.sqlite
5
+
6
+ class Haiku < ::Sequel::Model
7
+ set_schema do
8
+ primary_key :id
9
+ String :title
10
+ text :body
11
+ Boolean :published, :default => true
12
+ foreign_key :author_id, :authors
13
+ end
14
+ create_table unless table_exists?
15
+ many_to_one :author
16
+ def validate
17
+ errors[:title] << "Should start with a decent char" if title.to_s!='' && title[0]<65
18
+ end
19
+ end
20
+
21
+ class Author < ::Sequel::Model
22
+ set_schema do
23
+ primary_key :id
24
+ String :name, :crushyform=>{:required=>true}
25
+ String :surname, :crushyform=>{:required=>true}
26
+ end
27
+ create_table unless table_exists?
28
+ one_to_many :haikus
29
+ def validate
30
+ errors[:name] << 'Cannot be blank' if name.to_s==''
31
+ errors[:surname] << 'Cannot be blank' if surname.to_s==''
32
+ end
33
+ end
34
+
35
+ class Pic < ::Sequel::Model
36
+ set_schema do
37
+ primary_key :id
38
+ String :image, :crushyform=>{:type=>:attachment}
39
+ end
40
+ create_table unless table_exists?
41
+ end
42
+
43
+ Haiku.create( :title=>'Autumn', :body=>"Rust the ground\nFlush the branches\nReveal the trees" )
44
+ Haiku.create( :title=>'Winter', :body=>"There is snow\nIt covers you\nBut you are still the most beautiful" )
45
+ Haiku.create( :title=>'Spring', :body=>"No inspiration" )
46
+
47
+ Author.create(:name=>'Ray',:surname=>'Bradbury')
48
+ Author.create(:name=>'Jorge Luis',:surname=>'Borges')
49
+ Author.create(:name=>'Yasunari', :surname=>'Kawabata')
@@ -0,0 +1,160 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'rack'
4
+ require 'fileutils' # fix Rack missing
5
+
6
+ Bacon.summary_on_exit
7
+
8
+ # Helpers
9
+ F = ::File
10
+ D = ::Dir
11
+ ROOT = F.dirname(__FILE__)+'/..'
12
+ def req_lint(app)
13
+ ::Rack::MockRequest.new(::Rack::Lint.new(app))
14
+ end
15
+ dummy_app = proc{|env|[200,{'Content-Type'=>'text/plain'},['dummy']]}
16
+
17
+ $:.unshift ROOT+'/lib'
18
+ require ROOT+'/test/db.rb'
19
+ require 'backend_api'
20
+
21
+ def wrap(title, form) #mock wrapped versions of forms when not XHR
22
+ BackendAPI::WRAP % [title,form]
23
+ end
24
+
25
+ describe 'API Misc' do
26
+ should "Send 404 X-cascade if no response at the bottom of the Rack stack - Builder::run" do
27
+ res = req_lint(BackendAPI.new).get('/zzz')
28
+ res.status.should==404
29
+ res.headers['X-Cascade'].should=='pass'
30
+ end
31
+ should 'Follow the Rack stack if response is not found - Builder::use' do
32
+ res = req_lint(BackendAPI.new(dummy_app)).get('/')
33
+ res.status.should==200
34
+ res.body.should=='dummy'
35
+ end
36
+ should "Have a special path for sending version" do
37
+ res = req_lint(BackendAPI.new(dummy_app)).get('/_version')
38
+ res.status.should==200
39
+ res.body.should==BackendAPI::VERSION.join('.')
40
+ end
41
+ end
42
+
43
+ describe 'API Post' do
44
+ should "Create a new entry in the database and send a 201 response" do
45
+ 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"}})
46
+ res.status.should==201 # Created
47
+ haiku = Haiku.order(:id).last
48
+ haiku.title.should=='Summer'
49
+ haiku.body.should=="Summer was missing\nI cannot accept that\nI need to bake in the sun"
50
+ end
51
+ should "Fallback to an update if there is an id provided" do
52
+ req_lint(BackendAPI.new).post('/haiku/4', :params => {'model' => {'title' => 'Summer is not new !!!'}})
53
+ Haiku.filter(:title => 'Summer is not new !!!').first.id.should==4
54
+ end
55
+ should "Accept a new entry with no attributes as long as it is valid" do
56
+ res = req_lint(BackendAPI.new).post('/haiku')
57
+ res.status.should==201
58
+ end
59
+ should "Send back the appropriate form when the creation is not valid" do
60
+
61
+ res = req_lint(BackendAPI.new).post('/haiku', :params => {'model' => {'title' => '13'}})
62
+ res.status.should==400
63
+ compared = Haiku.new.set('title' => '13')
64
+ compared.valid?
65
+ res.body.should==wrap('Haiku', compared.backend_form('/haiku', ['title']))
66
+
67
+ # Not wrapped when XHR
68
+ res = req_lint(BackendAPI.new).post('/haiku', "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", :params => {'model' => {'title' => '13'}})
69
+ res.status.should==400
70
+ res.body.should==compared.backend_form('/haiku', ['title'])
71
+ end
72
+ should "Accept a destination for when Entry is validated and request is not XHR" do
73
+ res = req_lint(BackendAPI.new(dummy_app)).post('/haiku', :params => {'_destination' => 'http://www.domain.com/list.xml', 'model' => {'title' => 'Destination Summer'}})
74
+ res.status.should==302
75
+ res.headers['Location']=='http://www.domain.com/list.xml'
76
+ Haiku.order(:id).last.title.should=='Destination Summer'
77
+ end
78
+ should "keep destination until form is validated" do
79
+ req_lint(BackendAPI.new).post('/haiku', :params => {'_destination' => '/', 'model' => {'title' => '13'}}).body.should.match(/name='_destination'.*value='\/'/)
80
+ end
81
+ end
82
+
83
+ describe 'API Get' do
84
+ should "Return the form for a fresh entry when no id is provided" do
85
+ req_lint(BackendAPI.new).get('/haiku').body.should==wrap('Haiku', Haiku.new.backend_form('/haiku'))
86
+ end
87
+ should "Return the form for an update when id is provided" do
88
+ req_lint(BackendAPI.new).get('/haiku/3').body.should==wrap('Haiku', Haiku[3].backend_form('/haiku/3'))
89
+ end
90
+ should "Be able to send a form with selected set of fields" do
91
+ req_lint(BackendAPI.new).get('/haiku', :params => {'fields' => ['title']}).body.should==wrap('Haiku', Haiku.new.backend_form('/haiku', ['title']))
92
+ req_lint(BackendAPI.new).get('/haiku/3', :params => {'fields' => ['title']}).body.should==wrap('Haiku', Haiku[3].backend_form('/haiku/3', ['title']))
93
+ end
94
+ should "Update the entry before building the form if model parameter is used" do
95
+ update = {'title' => 'Changed'}
96
+ req_lint(BackendAPI.new).get('/haiku', :params => {'model' => update}).body.should==wrap('Haiku', Haiku.new.set(update).backend_form('/haiku'))
97
+ req_lint(BackendAPI.new).get('/haiku/3', :params => {'model' => update}).body.should==wrap('Haiku', Haiku[3].set(update).backend_form('/haiku/3'))
98
+ end
99
+ should "Return a partial if the request is XHR" do
100
+ req_lint(BackendAPI.new).get('/haiku', "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest").body.should==Haiku.new.backend_form('/haiku')
101
+ end
102
+ end
103
+
104
+ describe 'API Put' do
105
+ should "Update a database entry that exists and send a 201 response" do
106
+ 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"}})
107
+ res.status.should==201 # Created
108
+ haiku = Haiku[3]
109
+ haiku.body.should=="Maybe I have no inspiration\nBut at least\nIt should be on three lines"
110
+ haiku.title.should=='Spring'
111
+ end
112
+ should "Work with MethodOverride" do
113
+ req_lint(BackendAPI.new).post('/haiku/3', :params => {'_method' => 'PUT', 'model' => {'title' => 'Spring Wow !!!'}})
114
+ Haiku[3].title.should=='Spring Wow !!!'
115
+ end
116
+ should "Not break if one updates with no changes" do
117
+ res = req_lint(BackendAPI.new).put('/haiku/3')
118
+ res.status.should==201
119
+ end
120
+ should "Send back the appropriate form when the creation is not valid" do
121
+
122
+ res = req_lint(BackendAPI.new).put('/haiku/3', :params => {'model' => {'title' => '13'}})
123
+ res.status.should==400
124
+ compared = Haiku[3].set('title' => '13')
125
+ compared.valid?
126
+ res.body.should==wrap('Haiku', compared.backend_form('/haiku/3', ['title']))
127
+
128
+ # Not wrapped when XHR
129
+ res = req_lint(BackendAPI.new).put('/haiku/3', "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", :params => {'model' => {'title' => '13'}})
130
+ res.status.should==400
131
+ res.body.should==compared.backend_form('/haiku/3', ['title'])
132
+ end
133
+ should "Accept a destination for when Update is validated and request is not XHR" do
134
+ res = req_lint(BackendAPI.new(dummy_app)).post('/haiku/3', :params => {'_method' => 'PUT', '_destination' => '/', 'model' => {'title' => 'Spring destination !!!'}})
135
+ res.status.should==302
136
+ res.headers['Location']=='/'
137
+ Haiku[3].title.should=='Spring destination !!!'
138
+ end
139
+ should "keep destination until form is validated" do
140
+ req_lint(BackendAPI.new).put('/haiku/3', :params => {'_destination' => '/', 'model' => {'title' => '13'}}).body.should.match(/name='_destination'.*value='\/'/)
141
+ end
142
+ end
143
+
144
+ describe 'API Delete' do
145
+ should "Delete a database entry that exists and send a 204 response" do
146
+ res = req_lint(BackendAPI.new).delete('/haiku/1')
147
+ res.status.should==204 # No Content
148
+ Haiku[1].should==nil
149
+ end
150
+ should "Work with MethodOverride" do
151
+ req_lint(BackendAPI.new(dummy_app)).post('/haiku/2', :params => {'_method' => 'DELETE'})
152
+ Haiku[2].should==nil
153
+ end
154
+ should "Accept a destination" do
155
+ res = req_lint(BackendAPI.new).delete('/haiku/3', :params => {'_destination' => '/'})
156
+ res.status.should==302
157
+ res.headers['Location']=='/'
158
+ Haiku[3].should==nil
159
+ end
160
+ end
@@ -0,0 +1,79 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ Bacon.summary_on_exit
4
+
5
+ # Helpers
6
+ F = ::File
7
+ D = ::Dir
8
+ ROOT = F.dirname(__FILE__)+'/..'
9
+ $:.unshift ROOT+'/lib'
10
+ require ROOT+'/test/db.rb'
11
+ ::Sequel::Model.plugin :rack_backend_api_adapter
12
+
13
+ describe 'Sequel Adapter' do
14
+
15
+ should 'Define Model#default_backend_columns' do
16
+ Haiku.new.default_backend_columns.should==(Haiku.columns - [:id])
17
+ end
18
+
19
+ should 'Make forms for the correct action' do
20
+ Haiku.new.backend_form('/url').should.match(/action='\/url'/)
21
+ end
22
+
23
+ should 'Make forms including the requested fields - full form' do
24
+ haiku = Haiku.new
25
+ full_form = haiku.backend_form('/url')
26
+ haiku.default_backend_columns.each do |c|
27
+ full_form.should.match(/#{Regexp.escape haiku.crushyfield(c)}/)
28
+ end
29
+ end
30
+
31
+ should 'Make forms including the requested fields - partial form' do
32
+ haiku = Haiku.new
33
+ partial_form = haiku.backend_form('/url', [:title])
34
+ partial_form.should.match(/#{Regexp.escape haiku.crushyfield(:title)}/)
35
+ list = haiku.default_backend_columns - [:title]
36
+ list.each do |c|
37
+ partial_form.should.not.match(/#{Regexp.escape haiku.crushyfield(c)}/)
38
+ end
39
+ end
40
+
41
+ should 'Have Method Override value POST/PUT automatically set by default in the form' do
42
+ Haiku.new.backend_form('/url').should.match(/name='_method' value='POST'/)
43
+ Haiku.first.backend_form('/url').should.match(/name='_method' value='PUT'/)
44
+ end
45
+
46
+ should 'Not have a _destination field if the option is not used in the form' do
47
+ Haiku.new.backend_form('/url').should.not.match(/name='_destination'/)
48
+ end
49
+
50
+ should 'Have a _destination field if the option is used in the form' do
51
+ Haiku.new.backend_form('/url', nil, {:destination=>"/moon"}).should.match(/name='_destination' value='\/moon'/)
52
+ end
53
+
54
+ should 'Make forms with enctype automated' do
55
+ Haiku.new.backend_form('/url').should.not.match(/enctype='multipart\/form-data'/)
56
+ Pic.new.backend_form('/url').should.match(/enctype='multipart\/form-data'/)
57
+ end
58
+
59
+ should 'Be able to change text for the submit button of the form and keep it when validation does not pass straight away' do
60
+ f = Haiku.new.backend_form('/url')
61
+ f.should.match(/<input type='submit' name='save' value='SAVE' \/>/)
62
+ f.should.not.match(/name='_submit_text'/)
63
+ f = Haiku.new.backend_form('/url', nil, {:submit_text=>'CREATE'})
64
+ f.should.match(/<input type='submit' name='save' value='CREATE' \/>/)
65
+ f.should.match(/name='_submit_text' value='CREATE'/)
66
+ end
67
+
68
+ should 'Have a backend_delete_form method - pure HTTP way of deleting records with HTTP DELETE method' do
69
+ form = Haiku.first.backend_delete_form('/url')
70
+ form.should.match(/name='_method' value='DELETE'/)
71
+ form.should.match(/<input type='submit' name='save' value='X' \/>/)
72
+ form.scan(/input/).size.should==3
73
+ form = Haiku.first.backend_delete_form('/url', {:submit_text=>'Destroy', :destination=>'/moon'})
74
+ form.should.match(/<input type='submit' name='save' value='Destroy' \/>/)
75
+ form.should.match(/name='_destination' value='\/moon'/)
76
+ form.scan(/input/).size.should==4
77
+ end
78
+
79
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-backend-api
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Mickael Riga
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-21 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: The purpose of this Rack Middleware is to provide an API that interfaces with database actions in order to build a CMS.
23
+ email: mig@mypeplum.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - LICENSE
32
+ - README.md
33
+ - example/basic_admin.rb
34
+ - example/config.ru
35
+ - lib/backend_api.rb
36
+ - lib/sequel_rack_backend_api_adapter.rb
37
+ - rack-backend-api.gemspec
38
+ - test/db.rb
39
+ - test/spec_backend_api.rb
40
+ - test/spec_sequel_adapter.rb
41
+ has_rdoc: true
42
+ homepage: http://github.com/mig-hub/backend-api
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options: []
47
+
48
+ require_paths:
49
+ - ./lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ hash: 3
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ hash: 3
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ requirements: []
69
+
70
+ rubyforge_project:
71
+ rubygems_version: 1.4.2
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: A Rack middleware that provides a simple API for your Admin section
75
+ test_files: []
76
+