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.
@@ -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
+