sinatra-backbone-2 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +8 -0
- data/HISTORY.md +23 -0
- data/NOTES.md +18 -0
- data/README.md +51 -0
- data/Rakefile +34 -0
- data/examples/jstpages/app.rb +21 -0
- data/examples/jstpages/config.ru +3 -0
- data/examples/jstpages/public/app.js +20 -0
- data/examples/jstpages/views/hello.jst.tpl +6 -0
- data/examples/jstpages/views/home.erb +25 -0
- data/examples/restapi/app.rb +40 -0
- data/examples/restapi/config.ru +3 -0
- data/examples/restapi/home.erb +24 -0
- data/examples/restapi/public/app.js +105 -0
- data/lib/sinatra/backbone.rb +10 -0
- data/lib/sinatra/jstpages.rb +261 -0
- data/lib/sinatra/restapi.rb +299 -0
- data/sinatra-backbone.gemspec +20 -0
- data/test/app/views/chrome.jst.tpl +1 -0
- data/test/app/views/editor/edit.jst.jade +2 -0
- data/test/app_test.rb +62 -0
- data/test/arity_test.rb +44 -0
- data/test/emulate_test.rb +32 -0
- data/test/jst_test.rb +26 -0
- data/test/test_helper.rb +20 -0
- data/test/to_json_test.rb +44 -0
- metadata +169 -0
@@ -0,0 +1,299 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# ## RestAPI [module]
|
4
|
+
# A plugin for providing rest API to models. Great for Backbone.js.
|
5
|
+
#
|
6
|
+
# To use this, simply `register` it to your Sinatra Application. You can then
|
7
|
+
# use `rest_create` and `rest_resource` to create your routes.
|
8
|
+
#
|
9
|
+
# require 'sinatra/restapi'
|
10
|
+
#
|
11
|
+
# class App < Sinatra::Base
|
12
|
+
# register Sinatra::RestAPI
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# ### RestAPI example
|
16
|
+
# Here's a simple example of how to use Backbone models with RestAPI.
|
17
|
+
# Also see the [example application][ex] included in the gem.
|
18
|
+
#
|
19
|
+
# [ex]: https://github.com/rstacruz/sinatra-backbone/tree/master/examples/restapi
|
20
|
+
#
|
21
|
+
# #### Model setup
|
22
|
+
# Let's say you have a `Book` model in your application. Let's use [Sequel][sq]
|
23
|
+
# for this example, but feel free to use any other ORM that is
|
24
|
+
# ActiveModel-compatible.
|
25
|
+
#
|
26
|
+
# You will need to define `to_hash` in your model.
|
27
|
+
#
|
28
|
+
# db = Sequel.connect(...)
|
29
|
+
#
|
30
|
+
# db.create_table :books do
|
31
|
+
# primary_key :id
|
32
|
+
# String :title
|
33
|
+
# String :author
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# class Book < Sequel::Model
|
37
|
+
# # ...
|
38
|
+
# def to_hash
|
39
|
+
# { :title => title, :author => author, :id => id }
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# [sq]: http://sequel.rubyforge.org
|
44
|
+
#
|
45
|
+
# #### Sinatra
|
46
|
+
# To provide some routes for Backbone models, use `rest_resource` and
|
47
|
+
# `rest_create`:
|
48
|
+
#
|
49
|
+
# require 'sinatra/restapi'
|
50
|
+
#
|
51
|
+
# class App < Sinatra::Base
|
52
|
+
# register Sinatra::RestAPI
|
53
|
+
#
|
54
|
+
# rest_create '/book' do
|
55
|
+
# Book.new
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# rest_resource '/book/:id' do |id|
|
59
|
+
# Book.find(:id => id)
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# #### JavaScript
|
64
|
+
# In your JavaScript files, let's make a corresponding model.
|
65
|
+
#
|
66
|
+
# Book = Backbone.Model.extend({
|
67
|
+
# urlRoot: '/book'
|
68
|
+
# });
|
69
|
+
#
|
70
|
+
# Now you may create a new book through your JavaScript:
|
71
|
+
#
|
72
|
+
# book = new Book;
|
73
|
+
# book.set({ title: "Darkly Dreaming Dexter", author: "Jeff Lindsay" });
|
74
|
+
# book.save();
|
75
|
+
#
|
76
|
+
# // In Ruby, equivalent to:
|
77
|
+
# // book = Book.new
|
78
|
+
# // book.title = "Darkly Dreaming Dexter"
|
79
|
+
# // book.author = "Jeff Lindsay"
|
80
|
+
# // book.save
|
81
|
+
#
|
82
|
+
# Or you may retrieve new items. Note that in this example, since we defined
|
83
|
+
# `urlRoot()` but not `url()`, the model URL with default to `/[urlRoot]/[id]`.
|
84
|
+
#
|
85
|
+
# book = new Book({ id: 1 });
|
86
|
+
# book.fetch();
|
87
|
+
#
|
88
|
+
# // In Ruby, equivalent to:
|
89
|
+
# // Book.find(:id => 1)
|
90
|
+
#
|
91
|
+
# Deletes will work just like how you would expect it:
|
92
|
+
#
|
93
|
+
# book.destroy();
|
94
|
+
#
|
95
|
+
module Sinatra::RestAPI
|
96
|
+
def self.registered(app)
|
97
|
+
app.helpers Helpers
|
98
|
+
end
|
99
|
+
|
100
|
+
# ### rest_create(path, &block) [method]
|
101
|
+
# Creates a *create* route on the given `path`.
|
102
|
+
#
|
103
|
+
# This creates a `POST` route in */documents* that accepts JSON data.
|
104
|
+
# This route will return the created object as JSON.
|
105
|
+
#
|
106
|
+
# When getting a request, it does the following:
|
107
|
+
#
|
108
|
+
# * A new object is created by *yielding* the block you give. (Let's
|
109
|
+
# call it `object`.)
|
110
|
+
#
|
111
|
+
# * For each of the attributes, it uses the `attrib_name=` method in
|
112
|
+
# your record. For instance, for an attrib like `title`, it wil lbe
|
113
|
+
# calling `object.title = "hello"`.
|
114
|
+
#
|
115
|
+
# * if `object.valid?` returns false, it returns an error 400.
|
116
|
+
#
|
117
|
+
# * `object.save` will then be called.
|
118
|
+
#
|
119
|
+
# * `object`'s contents will then be returned to the client as JSON.
|
120
|
+
#
|
121
|
+
# See the example.
|
122
|
+
#
|
123
|
+
# class App < Sinatra::Base
|
124
|
+
# rest_create "/documents" do
|
125
|
+
# Document.new
|
126
|
+
# end
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
def rest_create(path, options={}, &blk)
|
130
|
+
# Create
|
131
|
+
post path do
|
132
|
+
@object = yield
|
133
|
+
rest_params.each { |k, v| @object.send :"#{k}=", v }
|
134
|
+
|
135
|
+
return 400, @object.errors.to_json unless @object.valid?
|
136
|
+
|
137
|
+
@object.save
|
138
|
+
rest_respond @object.to_hash
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# ### rest_resource(path, &block) [method]
|
143
|
+
# Creates a *get*, *edit* and *delete* route on the given `path`.
|
144
|
+
#
|
145
|
+
# The block given will be yielded to do a record lookup. If the block returns
|
146
|
+
# `nil`, RestAPI will return a *404*.
|
147
|
+
#
|
148
|
+
# In the example, it creates routes for `/document/:id` to accept HTTP *GET*
|
149
|
+
# (for object retrieval), *PUT* (for editing), and *DELETE* (for destroying).
|
150
|
+
#
|
151
|
+
# Your model needs to implement the following methods:
|
152
|
+
#
|
153
|
+
# * `save` (called on edit)
|
154
|
+
# * `destroy` (called on delete)
|
155
|
+
# * `<attrib_name_here>=` (called for each of the attributes on edit)
|
156
|
+
#
|
157
|
+
# If you only want to create routes for only one or two of the actions, you
|
158
|
+
# may individually use:
|
159
|
+
#
|
160
|
+
# * `rest_get`
|
161
|
+
# * `rest_edit`
|
162
|
+
# * `rest_delete`
|
163
|
+
#
|
164
|
+
# All the methods above take the same arguments as `rest_resource`.
|
165
|
+
#
|
166
|
+
# class App < Sinatra::Base
|
167
|
+
# rest_resource "/document/:id" do |id|
|
168
|
+
# Document.find(:id => id)
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
def rest_resource(path, options={}, &blk)
|
173
|
+
rest_get path, options, &blk
|
174
|
+
rest_edit path, options, &blk
|
175
|
+
rest_delete path, options, &blk
|
176
|
+
end
|
177
|
+
|
178
|
+
# ### rest_get(path, &block) [method]
|
179
|
+
# This is the same as `rest_resource`, but only handles *GET* requests.
|
180
|
+
#
|
181
|
+
def rest_get(path, options={}, &blk)
|
182
|
+
get path do |*args|
|
183
|
+
@object = yield(*args) or pass
|
184
|
+
rest_respond @object
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# ### rest_edit(path, &block) [method]
|
189
|
+
# This is the same as `rest_resource`, but only handles *PUT*/*POST* (edit)
|
190
|
+
# requests.
|
191
|
+
#
|
192
|
+
def rest_edit(path, options={}, &blk)
|
193
|
+
callback = Proc.new { |*args|
|
194
|
+
@object = yield(*args) or pass
|
195
|
+
rest_params.each { |k, v| @object.send :"#{k}=", v unless k == 'id' }
|
196
|
+
|
197
|
+
return 400, @object.errors.to_json unless @object.valid?
|
198
|
+
|
199
|
+
@object.save
|
200
|
+
rest_respond @object
|
201
|
+
}
|
202
|
+
|
203
|
+
# Make it work with `Backbone.emulateHTTP` on.
|
204
|
+
put path, &callback
|
205
|
+
post path, &callback
|
206
|
+
end
|
207
|
+
|
208
|
+
# ### rest_delete(path, &block) [method]
|
209
|
+
# This is the same as `rest_resource`, but only handles *DELETE* (edit)
|
210
|
+
# requests. This uses `Model#destroy` on your model.
|
211
|
+
#
|
212
|
+
def rest_delete(path, options={}, &blk)
|
213
|
+
delete path do |*args|
|
214
|
+
@object = yield(*args) or pass
|
215
|
+
@object.destroy
|
216
|
+
rest_respond :result => :success
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# ### JSON conversion
|
221
|
+
#
|
222
|
+
# The *create* and *get* routes all need to return objects as JSON. RestAPI
|
223
|
+
# attempts to convert your model instances to JSON by first trying
|
224
|
+
# `object.to_json` on it, then trying `object.to_hash.to_json`.
|
225
|
+
#
|
226
|
+
# You will need to implement `#to_hash` or `#to_json` in your models.
|
227
|
+
#
|
228
|
+
# class Album < Sequel::Model
|
229
|
+
# def to_hash
|
230
|
+
# { :id => id,
|
231
|
+
# :title => title,
|
232
|
+
# :artist => artist,
|
233
|
+
# :year => year }
|
234
|
+
# end
|
235
|
+
# end
|
236
|
+
|
237
|
+
# ### Helper methods
|
238
|
+
# There are some helper methods that are used internally be `RestAPI`,
|
239
|
+
# but you can use them too if you need them.
|
240
|
+
#
|
241
|
+
module Helpers
|
242
|
+
# #### rest_respond(object)
|
243
|
+
# Responds with a request with the given `object`.
|
244
|
+
#
|
245
|
+
# This will convert that object to either JSON or XML as needed, depending
|
246
|
+
# on the client's preferred type (dictated by the HTTP *Accepts* header).
|
247
|
+
#
|
248
|
+
def rest_respond(obj)
|
249
|
+
case request.preferred_type('*/json', '*/xml')
|
250
|
+
when '*/json'
|
251
|
+
content_type :json
|
252
|
+
rest_convert_to_json obj
|
253
|
+
|
254
|
+
else
|
255
|
+
pass
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# #### rest_params
|
260
|
+
# Returns the object from the request.
|
261
|
+
#
|
262
|
+
# If the client sent `application/json` (or `text/json`) as the content
|
263
|
+
# type, it tries to parse the request body as JSON.
|
264
|
+
#
|
265
|
+
# If the client sent a standard URL-encoded POST with a `model` key
|
266
|
+
# (happens when Backbone uses `Backbone.emulateJSON = true`), it tries
|
267
|
+
# to parse its value as JSON.
|
268
|
+
#
|
269
|
+
# Otherwise, the params will be returned as is.
|
270
|
+
#
|
271
|
+
def rest_params
|
272
|
+
if File.fnmatch('*/json', request.content_type)
|
273
|
+
JSON.parse request.body.read
|
274
|
+
|
275
|
+
elsif params['model']
|
276
|
+
# Account for Backbone.emulateJSON.
|
277
|
+
JSON.parse params['model']
|
278
|
+
|
279
|
+
else
|
280
|
+
params
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def rest_convert_to_json(obj)
|
285
|
+
# Convert to JSON. This will almost always work as the JSON lib adds
|
286
|
+
# #to_json to everything.
|
287
|
+
json = obj.to_json
|
288
|
+
|
289
|
+
# The default to_json of objects is to JSONify the #to_s of an object,
|
290
|
+
# which defaults to #inspect. We don't want that.
|
291
|
+
return json unless json[0..2] == '"#<'
|
292
|
+
|
293
|
+
# Let's hope they redefined to_hash.
|
294
|
+
return obj.to_hash.to_json if obj.respond_to?(:to_hash)
|
295
|
+
|
296
|
+
raise "Can't convert object to JSON. Consider implementing #to_hash to #{obj.class.name}."
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require './lib/sinatra/backbone'
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = "sinatra-backbone-2"
|
4
|
+
s.version = Sinatra::Backbone.version
|
5
|
+
s.summary = "Helpful stuff using Sinatra with Backbone."
|
6
|
+
s.description = "Provides Rest API access to your models and serves JST pages."
|
7
|
+
s.authors = ["Rico Sta. Cruz"]
|
8
|
+
s.email = ["rico@sinefunc.com"]
|
9
|
+
s.homepage = "http://github.com/rstacruz/sinatra-backbone"
|
10
|
+
s.files = `git ls-files`.strip.split("\n")
|
11
|
+
s.executables = Dir["bin/*"].map { |f| File.basename(f) }
|
12
|
+
|
13
|
+
s.add_dependency "sinatra"
|
14
|
+
s.add_development_dependency "rake"
|
15
|
+
s.add_development_dependency "sequel", ">= 3.25.0"
|
16
|
+
s.add_development_dependency "sqlite3", "~> 1.3.4"
|
17
|
+
s.add_development_dependency "contest", "~> 0.1.3"
|
18
|
+
s.add_development_dependency "mocha", "~> 0.13.3"
|
19
|
+
s.add_development_dependency "rack-test", "~> 0.6.2"
|
20
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= chrome %>
|
data/test/app_test.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
DB.create_table :books do
|
4
|
+
primary_key :id
|
5
|
+
String :name
|
6
|
+
String :author
|
7
|
+
end
|
8
|
+
|
9
|
+
class Book < Sequel::Model
|
10
|
+
def to_hash
|
11
|
+
{ :name => name, :author => author }
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate
|
15
|
+
super
|
16
|
+
errors.add(:author, "can't be empty") if author.to_s.size == 0
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class AppTest < UnitTest
|
21
|
+
class App < Sinatra::Base
|
22
|
+
register Sinatra::RestAPI
|
23
|
+
disable :show_exceptions
|
24
|
+
enable :raise_errors
|
25
|
+
rest_create("/book") { Book.new }
|
26
|
+
rest_resource("/book/:id") { |id| Book[id] }
|
27
|
+
end
|
28
|
+
def app() App; end
|
29
|
+
|
30
|
+
describe "Sinatra::RestAPI" do
|
31
|
+
setup do
|
32
|
+
@book = Book.new
|
33
|
+
@book.name = "Darkly Dreaming Dexter"
|
34
|
+
@book.author = "Jeff Lindsay"
|
35
|
+
@book.save
|
36
|
+
header 'Accept', 'application/json, */*'
|
37
|
+
end
|
38
|
+
|
39
|
+
teardown do
|
40
|
+
@book.destroy if Book[@book.id]
|
41
|
+
end
|
42
|
+
|
43
|
+
test "should work properly" do
|
44
|
+
get "/book/#{@book.id}"
|
45
|
+
|
46
|
+
assert json_response['name'] == @book.name
|
47
|
+
assert json_response['author'] == @book.author
|
48
|
+
end
|
49
|
+
|
50
|
+
test "validation fail" do
|
51
|
+
hash = { :name => "The Claiming of Sleeping Beauty" }
|
52
|
+
post "/book", :model => hash.to_json
|
53
|
+
assert last_response.status != 200
|
54
|
+
end
|
55
|
+
|
56
|
+
test "should 404" do
|
57
|
+
get "/book/823978"
|
58
|
+
|
59
|
+
assert last_response.status == 404
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/test/arity_test.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class ArityTest < UnitTest
|
4
|
+
class FauxModel
|
5
|
+
def initialize(stuff)
|
6
|
+
@stuff = stuff
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_hash
|
10
|
+
{ :contents => @stuff }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class App < Sinatra::Base
|
15
|
+
register Sinatra::RestAPI
|
16
|
+
disable :show_exceptions
|
17
|
+
enable :raise_errors
|
18
|
+
|
19
|
+
rest_resource("/api/:x/:y/:z") { |x, y, z| FauxModel.new ["Hello", x.to_i+1, y.to_i+1, z.to_i+1] }
|
20
|
+
end
|
21
|
+
|
22
|
+
def app() App; end
|
23
|
+
|
24
|
+
describe "Multi args support" do
|
25
|
+
test "get" do
|
26
|
+
header 'Accept', 'application/json, */*'
|
27
|
+
get "/api/20/40/60"
|
28
|
+
|
29
|
+
assert json_response["contents"] = ["Hello", 21, 41, 61]
|
30
|
+
end
|
31
|
+
|
32
|
+
test "put/post" do
|
33
|
+
FauxModel.any_instance.expects(:x=).times(1).returns(true)
|
34
|
+
FauxModel.any_instance.expects(:valid?).times(1).returns(true)
|
35
|
+
FauxModel.any_instance.expects(:save).times(1).returns(true)
|
36
|
+
|
37
|
+
header 'Accept', 'application/json, */*'
|
38
|
+
header 'Content-Type', 'application/json'
|
39
|
+
post "/api/20/40/60", JSON.generate('x' => 2)
|
40
|
+
|
41
|
+
assert json_response["contents"] = ["Hello", 21, 41, 61]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
class EmulateTest < UnitTest
|
5
|
+
class App < Sinatra::Base
|
6
|
+
register Sinatra::RestAPI
|
7
|
+
disable :show_exceptions
|
8
|
+
enable :raise_errors
|
9
|
+
|
10
|
+
rest_resource("/api/:id") { |id| FauxModel.new }
|
11
|
+
end
|
12
|
+
|
13
|
+
def app() App; end
|
14
|
+
|
15
|
+
setup do
|
16
|
+
header 'Accept', 'application/json, */*'
|
17
|
+
end
|
18
|
+
|
19
|
+
test "emulate json and emulate http" do
|
20
|
+
FauxModel.any_instance.expects(:two=).times(1).returns(true)
|
21
|
+
FauxModel.any_instance.expects(:save).times(1).returns(true)
|
22
|
+
FauxModel.any_instance.expects(:valid?).times(1).returns(true)
|
23
|
+
FauxModel.any_instance.expects(:to_hash).times(1).returns('a' => 'b')
|
24
|
+
|
25
|
+
post "/api/2", :model => { :two => 2 }.to_json
|
26
|
+
assert json_response == { 'a' => 'b' }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class FauxModel
|
31
|
+
end
|
32
|
+
|