sinatra-backbone 0.1.0.rc1 → 0.1.0.rc2

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/HISTORY.md CHANGED
@@ -1,3 +1,12 @@
1
+ v0.1.0.rc2 - Sep 12, 2011
2
+ -------------------------
3
+
4
+ ### Changed:
5
+ * Check for validity of models on the server, and add an example.
6
+ * Added examples in the documentation.
7
+ * Support multiple args in routes.
8
+ * Added more tests.
9
+
1
10
  v0.1.0.rc1 - Sept 12, 2011
2
11
  --------------------------
3
12
 
@@ -0,0 +1,40 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'sinatra/base'
4
+ require 'sequel'
5
+ require 'sinatra/backbone'
6
+
7
+ DB = Sequel.connect("sqlite::memory:")
8
+ DB.create_table :books do
9
+ primary_key :id
10
+ String :title
11
+ String :author
12
+ end
13
+
14
+ class Book < Sequel::Model
15
+ def to_hash
16
+ { :id => id, :title => title, :author => author }
17
+ end
18
+
19
+ def validate
20
+ errors.add :author, "can't be empty" if author.to_s.size == 0
21
+ end
22
+ end
23
+
24
+ class App < Sinatra::Base
25
+ enable :raise_errors, :logging
26
+ enable :show_exceptions if development?
27
+
28
+ register Sinatra::RestAPI
29
+
30
+ rest_create("/book") { Book.new }
31
+ rest_resource("/book/:id") { |id| Book[id] }
32
+
33
+ set :root, File.expand_path('../', __FILE__)
34
+ set :views, File.expand_path('../', __FILE__)
35
+ set :public, File.expand_path('../public', __FILE__)
36
+
37
+ get '/' do
38
+ erb :home
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ require './app'
2
+ App.set :run, false
3
+ run App
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title></title>
6
+ <script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/1.6.2/jquery.min.js'></script>
7
+ <script src='http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.1.7/underscore-min.js'></script>
8
+ <script src='http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.5.3/backbone-min.js'></script>
9
+ <style>
10
+ body { font-family: sans-serif; font-size: 13px; line-height: 1.5; background: #d0d0da; }
11
+ #messages { width: 400px; margin: 20px auto; background: white; padding: 20px; border: solid 10px #c0c0ca; }
12
+ h3 { border-top: dotted 1px #ccc; padding: 20px 20px 0 20px; margin: 20px -20px 0 -20px; color: #46a; }
13
+ h3:first-child { border-top: 0; margin-top: 0; padding-top: 0; }
14
+ dl { overflow: hidden; }
15
+ dt { float: left; width: 120px; margin-right: 10px; text-align: right; color: #aaa; }
16
+ h3 { font-family: palatino; font-size: 1.3em; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div id="messages">
21
+ </div>
22
+ <script src='app.js'></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,105 @@
1
+ // Here is our Backbone model!
2
+ Book = Backbone.Model.extend({
3
+ urlRoot: '/book'
4
+ });
5
+
6
+ $(function() {
7
+ do_create();
8
+ });
9
+
10
+ function do_create() {
11
+ echo("<h3>Creating a book:</h3>");
12
+
13
+ var book = new Book;
14
+ book.set({ title: "Darkly Dreaming Dexter", author: "Jeff Lindsay" });
15
+ book.save({}, {
16
+ error: onerror,
17
+ success: function() {
18
+ print_book(book);
19
+ echo("<h3>Retrieving the same book:</h3>");
20
+ do_retrieve(book);
21
+ }
22
+ });
23
+ }
24
+
25
+ function do_retrieve(_book) {
26
+ var book = new Book({ id: _book.id });
27
+ book.fetch({
28
+ error: onerror,
29
+ success: function() {
30
+ print_book(book);
31
+ do_edit_error(book);
32
+ }
33
+ });
34
+ }
35
+
36
+ function do_edit_error(book) {
37
+ echo("<h3>Editing book with an error:</h3>");
38
+ console.log("(You should see an HTTP error right about here:)");
39
+ book.set({ author: '' });
40
+ book.save({}, {
41
+ success: onerror,
42
+ error: function() {
43
+ console.log("(...yep.)");
44
+ echo("...yes, it occured.");
45
+ do_edit(book);
46
+ }
47
+ });
48
+ }
49
+
50
+ function do_edit(book) {
51
+ echo("<h3>Editing book:</h3>");
52
+ book.set({ author: 'Anne Rice', title: 'The Claiming of Sleeping Beauty' });
53
+ book.save({}, {
54
+ error: onerror,
55
+ success: function() {
56
+ print_book(book);
57
+ do_delete(book);
58
+ }
59
+ });
60
+ }
61
+
62
+ function do_delete(book) {
63
+ echo("<h3>Deleting book:</h3>");
64
+ book.destroy({
65
+ error: onerror,
66
+ success: function() {
67
+ echo("Success.");
68
+ do_verify_delete(book.id);
69
+ }
70
+ });
71
+ }
72
+
73
+ function do_verify_delete(id) {
74
+ echo("<h3>Checking if book "+id+" still exists:</h3>");
75
+ console.log("(You should see an HTTP error right about here:)");
76
+ var book = new Book({ id: id });
77
+ book.fetch({
78
+ success: onerror,
79
+ error: function() {
80
+ console.log("(...yep.)");
81
+ echo("No, it doesn't.");
82
+ do_success();
83
+ }
84
+ });
85
+ }
86
+
87
+ function do_success() {
88
+ echo("<h3>Success!</h3>");
89
+ }
90
+
91
+ function print_book(book) {
92
+ echo("<dl><dt>Title:</dt><dd>"+book.get('title')+"</dd></dl>");
93
+ echo("<dl><dt>Author:</dt><dd>"+book.get('author')+"</dd></dl>");
94
+ echo("<dl><dt>ID:</dt><dd>"+book.get('id')+"</dd></dl>");
95
+ }
96
+
97
+ // Helper functions
98
+ function echo(html) {
99
+ $("#messages").append(html);
100
+ };
101
+
102
+ function onerror() {
103
+ echo("<p class='error'>Oops... an error occured.</p>");
104
+ };
105
+
@@ -1,9 +1,8 @@
1
1
  module Sinatra
2
2
  module Backbone
3
3
  def self.version
4
- "0.1.0.rc1"
4
+ "0.1.0.rc2"
5
5
  end
6
-
7
6
  end
8
7
 
9
8
  autoload :RestAPI, "sinatra/restapi"
@@ -144,7 +144,7 @@ end
144
144
  #
145
145
  # module Sinatra::JstPages
146
146
  # class MyEngine < Engine
147
- # def function() "My.compile(%s)"; end
147
+ # def function() "My.compile(#{contents.inspect})"; end
148
148
  # end
149
149
  #
150
150
  # register 'my', MyEngine
@@ -12,6 +12,86 @@ require 'json'
12
12
  # register Sinatra::RestAPI
13
13
  # end
14
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
+ #
15
95
  module Sinatra::RestAPI
16
96
  def self.registered(app)
17
97
  app.helpers Helpers
@@ -32,7 +112,9 @@ module Sinatra::RestAPI
32
112
  # your record. For instance, for an attrib like `title`, it wil lbe
33
113
  # calling `object.title = "hello"`.
34
114
  #
35
- # * `object.save` will be called.
115
+ # * if `object.valid?` returns false, it returns an error 400.
116
+ #
117
+ # * `object.save` will then be called.
36
118
  #
37
119
  # * `object`'s contents will then be returned to the client as JSON.
38
120
  #
@@ -49,6 +131,9 @@ module Sinatra::RestAPI
49
131
  post path do
50
132
  @object = yield
51
133
  rest_params.each { |k, v| @object.send :"#{k}=", v }
134
+
135
+ return 400, @object.errors.to_json unless @object.valid?
136
+
52
137
  @object.save
53
138
  rest_respond @object.to_hash
54
139
  end
@@ -79,8 +164,8 @@ module Sinatra::RestAPI
79
164
  # All the methods above take the same arguments as `rest_resource`.
80
165
  #
81
166
  # class App < Sinatra::Base
82
- # rest_resource "/document/:id" do
83
- # Document.find(id)
167
+ # rest_resource "/document/:id" do |id|
168
+ # Document.find(:id => id)
84
169
  # end
85
170
  # end
86
171
  #
@@ -94,8 +179,8 @@ module Sinatra::RestAPI
94
179
  # This is the same as `rest_resource`, but only handles *GET* requests.
95
180
  #
96
181
  def rest_get(path, options={}, &blk)
97
- get path do |id|
98
- @object = yield(id) or pass
182
+ get path do |*args|
183
+ @object = yield(*args) or pass
99
184
  rest_respond @object
100
185
  end
101
186
  end
@@ -105,9 +190,12 @@ module Sinatra::RestAPI
105
190
  # requests.
106
191
  #
107
192
  def rest_edit(path, options={}, &blk)
108
- callback = Proc.new { |id|
109
- @object = yield(id) or pass
193
+ callback = Proc.new { |*args|
194
+ @object = yield(*args) or pass
110
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
+
111
199
  @object.save
112
200
  rest_respond @object
113
201
  }
@@ -122,8 +210,8 @@ module Sinatra::RestAPI
122
210
  # requests. This uses `Model#destroy` on your model.
123
211
  #
124
212
  def rest_delete(path, options={}, &blk)
125
- delete path do |id|
126
- @object = yield(id) or pass
213
+ delete path do |*args|
214
+ @object = yield(*args) or pass
127
215
  @object.destroy
128
216
  rest_respond :result => :success
129
217
  end
@@ -167,7 +255,7 @@ module Sinatra::RestAPI
167
255
  #
168
256
  # If the client sent a standard URL-encoded POST with a `model` key
169
257
  # (happens when Backbone uses `Backbone.emulateJSON = true`), it tries
170
- # to parse it's key as JSON.
258
+ # to parse its value as JSON.
171
259
  #
172
260
  # Otherwise, the params will be returned as is.
173
261
  #
@@ -14,5 +14,6 @@ Gem::Specification.new do |s|
14
14
  s.add_development_dependency "sequel", ">= 3.25.0"
15
15
  s.add_development_dependency "sqlite3", ">= 1.3.4"
16
16
  s.add_development_dependency "contest"
17
+ s.add_development_dependency "mocha"
17
18
  s.add_development_dependency "rack-test"
18
19
  end
data/test/app_test.rb CHANGED
@@ -10,6 +10,11 @@ class Book < Sequel::Model
10
10
  def to_hash
11
11
  { :name => name, :author => author }
12
12
  end
13
+
14
+ def validate
15
+ super
16
+ errors.add(:author, "can't be empty") if author.to_s.size == 0
17
+ end
13
18
  end
14
19
 
15
20
  class AppTest < UnitTest
@@ -17,6 +22,7 @@ class AppTest < UnitTest
17
22
  register Sinatra::RestAPI
18
23
  disable :show_exceptions
19
24
  enable :raise_errors
25
+ rest_create("/book") { Book.new }
20
26
  rest_resource("/book/:id") { |id| Book[id] }
21
27
  end
22
28
  def app() App; end
@@ -41,6 +47,12 @@ class AppTest < UnitTest
41
47
  assert json_response['author'] == @book.author
42
48
  end
43
49
 
50
+ test "validation fail" do
51
+ hash = { :name => "The Claiming of Sleeping Beauty" }
52
+ post "/book", :model => hash.to_json
53
+ p last_response
54
+ end
55
+
44
56
  test "should 404" do
45
57
  get "/book/823978"
46
58
 
@@ -0,0 +1,43 @@
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(:save).times(1).returns(true)
35
+
36
+ header 'Accept', 'application/json, */*'
37
+ header 'Content-Type', 'application/json'
38
+ post "/api/20/40/60", JSON.generate('x' => 2)
39
+
40
+ assert json_response["contents"] = ["Hello", 21, 41, 61]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class EmulateTest < UnitTest
4
+ class App < Sinatra::Base
5
+ register Sinatra::RestAPI
6
+ disable :show_exceptions
7
+ enable :raise_errors
8
+
9
+ rest_resource("/api/:id") { |id| FauxModel.new id }
10
+ end
11
+
12
+ def app() App; end
13
+
14
+ setup do
15
+ header 'Accept', 'application/json, */*'
16
+ end
17
+
18
+ test "emulate json and emulate http" do
19
+ FauxModel.any_instance.expects(:two=).times(1).returns(true)
20
+ FauxModel.any_instance.expects(:save).times(1).returns(true)
21
+ FauxModel.any_instance.expects(:to_hash).times(1).returns('a' => 'b')
22
+
23
+ post "/api/2", :model => { :two => 2 }.to_json
24
+ assert json_response == { 'a' => 'b' }
25
+ end
26
+ end
27
+
28
+ class FauxModel
29
+ end
30
+
data/test/test_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  $:.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'contest'
3
+ require 'mocha'
3
4
  require 'sinatra/base'
4
5
  require 'sequel'
5
6
  require 'rack/test'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-backbone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.rc1
4
+ version: 0.1.0.rc2
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -14,7 +14,7 @@ default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: sinatra
17
- requirement: &2161564340 !ruby/object:Gem::Requirement
17
+ requirement: &2165346540 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: '0'
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2161564340
25
+ version_requirements: *2165346540
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: sequel
28
- requirement: &2161563580 !ruby/object:Gem::Requirement
28
+ requirement: &2165345800 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: 3.25.0
34
34
  type: :development
35
35
  prerelease: false
36
- version_requirements: *2161563580
36
+ version_requirements: *2165345800
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: sqlite3
39
- requirement: &2161562920 !ruby/object:Gem::Requirement
39
+ requirement: &2165345140 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ! '>='
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: 1.3.4
45
45
  type: :development
46
46
  prerelease: false
47
- version_requirements: *2161562920
47
+ version_requirements: *2165345140
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: contest
50
- requirement: &2161562440 !ruby/object:Gem::Requirement
50
+ requirement: &2165344580 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ! '>='
@@ -55,10 +55,21 @@ dependencies:
55
55
  version: '0'
56
56
  type: :development
57
57
  prerelease: false
58
- version_requirements: *2161562440
58
+ version_requirements: *2165344580
59
+ - !ruby/object:Gem::Dependency
60
+ name: mocha
61
+ requirement: &2165343740 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: *2165343740
59
70
  - !ruby/object:Gem::Dependency
60
71
  name: rack-test
61
- requirement: &2161561800 !ruby/object:Gem::Requirement
72
+ requirement: &2165343140 !ruby/object:Gem::Requirement
62
73
  none: false
63
74
  requirements:
64
75
  - - ! '>='
@@ -66,7 +77,7 @@ dependencies:
66
77
  version: '0'
67
78
  type: :development
68
79
  prerelease: false
69
- version_requirements: *2161561800
80
+ version_requirements: *2165343140
70
81
  description: Provides Rest API access to your models and serves JST pages.
71
82
  email:
72
83
  - rico@sinefunc.com
@@ -78,6 +89,10 @@ files:
78
89
  - HISTORY.md
79
90
  - README.md
80
91
  - Rakefile
92
+ - examples/restapi/app.rb
93
+ - examples/restapi/config.ru
94
+ - examples/restapi/home.erb
95
+ - examples/restapi/public/app.js
81
96
  - lib/sinatra/backbone.rb
82
97
  - lib/sinatra/jstpages.rb
83
98
  - lib/sinatra/restapi.rb
@@ -85,6 +100,8 @@ files:
85
100
  - test/app/views/chrome.jst.tpl
86
101
  - test/app/views/editor/edit.jst.jade
87
102
  - test/app_test.rb
103
+ - test/arity_test.rb
104
+ - test/emulate_test.rb
88
105
  - test/jst_test.rb
89
106
  - test/test_helper.rb
90
107
  - test/to_json_test.rb