sinatra-backbone-2 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 %>
@@ -0,0 +1,2 @@
1
+ h1
2
+ | Hello
@@ -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
@@ -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
+