sinatra-backbone 0.1.0.rc1 → 0.1.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
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