remotely 0.0.4

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,3 @@
1
+ module Remotely
2
+ VERSION = "0.0.4"
3
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/remotely/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Matte Noble"]
6
+ gem.email = ["me@mattenoble.com"]
7
+ gem.description = %q{Remote API based model associations.}
8
+ gem.summary = %q{Remote API based model associations.}
9
+ gem.homepage = ''
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "remotely"
15
+ gem.require_paths = ['lib']
16
+ gem.version = Remotely::VERSION
17
+
18
+ gem.add_development_dependency "rake"
19
+ gem.add_development_dependency "rspec", "~> 2.6.0"
20
+ gem.add_development_dependency "ZenTest"
21
+ gem.add_development_dependency "autotest-growl"
22
+ gem.add_development_dependency "webmock"
23
+ gem.add_development_dependency "ruby-debug19"
24
+ gem.add_development_dependency "ruby-debug-completion"
25
+
26
+ gem.add_dependency "activesupport"
27
+ gem.add_dependency "activemodel"
28
+ gem.add_dependency "faraday", "~> 0.7.4"
29
+ gem.add_dependency "yajl-ruby", "~> 0.8.2"
30
+ end
@@ -0,0 +1,146 @@
1
+ require "spec_helper"
2
+
3
+ describe Remotely::Associations do
4
+ let(:app) { "http://localhost:1234" }
5
+
6
+ shared_examples_for "an association" do
7
+ it "keeps track of it's remote associations" do
8
+ subject.remote_associations.should include(assoc)
9
+ end
10
+
11
+ it "creates a method for the association" do
12
+ subject.should respond_to(assoc)
13
+ end
14
+
15
+ it "creates a setter for the associations" do
16
+ subject.public_send(:"#{assoc}=", "guy")
17
+ subject.public_send(assoc).should == "guy"
18
+ end
19
+ end
20
+
21
+ shared_examples_for "an association with a path" do
22
+ it "generates the correct path" do
23
+ subject.path_to(assoc, type).should == path
24
+ end
25
+
26
+ it "requests the correct path" do
27
+ subject.send(assoc)
28
+ a_request(:get, "#{app}#{path}").should have_been_made
29
+ end
30
+ end
31
+
32
+ describe "has_many_remote" do
33
+ subject { HasMany.new(id: 1) }
34
+ let(:type) { :has_many }
35
+ let(:assoc) { :things }
36
+
37
+ it_behaves_like "an association"
38
+
39
+ it "returns a Collection" do
40
+ subject.things.should be_a Remotely::Collection
41
+ end
42
+
43
+ it "returns a Collection of the appropriate model" do
44
+ subject.things.first.should be_a Thing
45
+ end
46
+
47
+ it "returns nil when it can not fetch the association" do
48
+ subject.id = nil
49
+ subject.things.should be_nil
50
+ end
51
+
52
+ context "with no options" do
53
+ subject { HasMany.new(id: 1) }
54
+ let(:path) { "/has_manies/1/things" }
55
+ it_behaves_like "an association with a path"
56
+ end
57
+
58
+ context "with the :path option" do
59
+ subject { HasManyWithPath.new(id: 1) }
60
+ let(:path) { "/custom/things" }
61
+ it_behaves_like "an association with a path"
62
+ end
63
+
64
+ context "with :path variables" do
65
+ subject { HasManyWithPathVariables.new(name: "stuff") }
66
+ let(:path) { "/custom/stuff/things" }
67
+ it_behaves_like "an association with a path"
68
+ end
69
+
70
+ context "with the :foreign_key option" do
71
+ subject { HasManyWithForeignKey.new }
72
+ specify { expect { subject.path_to(:things, :has_many) }.to raise_error(Remotely::HasManyForeignKeyError) }
73
+ end
74
+ end
75
+
76
+ describe "has_one_remote" do
77
+ subject { HasOne.new(id: 1) }
78
+ let(:type) { :has_one }
79
+ let(:assoc) { :thing }
80
+
81
+ it_behaves_like "an association"
82
+
83
+ it "returns an object of the appropriate model" do
84
+ subject.thing.should be_a Thing
85
+ end
86
+
87
+ context "with no options" do
88
+ subject { HasOne.new(id: 1) }
89
+ let(:path) { "/has_ones/1/thing" }
90
+ it_behaves_like "an association with a path"
91
+ end
92
+
93
+ context "with the :path option" do
94
+ subject { HasOneWithPath.new(id: 1) }
95
+ let(:path) { "/custom/thing" }
96
+ it_behaves_like "an association with a path"
97
+ end
98
+
99
+ context "with :path variables" do
100
+ subject { HasOneWithPathVariables.new(name: "stuff") }
101
+ let(:path) { "/custom/stuff/thing" }
102
+ it_behaves_like "an association with a path"
103
+ end
104
+
105
+ context "with the :foreign_key option" do
106
+ subject { HasOneWithForeignKey.new }
107
+ specify { expect { subject.path_to(:thing, :has_one) }.to raise_error(Remotely::HasManyForeignKeyError) }
108
+ end
109
+ end
110
+
111
+ describe "belongs_to_remote" do
112
+ subject { BelongsTo.new(id: 1, thing_id: 1) }
113
+ let(:type) { :belongs_to }
114
+ let(:assoc) { :thing }
115
+
116
+ it_behaves_like "an association"
117
+
118
+ it "returns an object of the appropriate model" do
119
+ subject.thing.should be_a Thing
120
+ end
121
+
122
+ it "should not fetch an association when the foreign key is nil" do
123
+ subject.thing_id = nil
124
+ subject.thing.should be_nil
125
+ a_request(:get, %r(/things)).should_not have_been_made
126
+ end
127
+
128
+ context "with no options" do
129
+ subject { BelongsTo.new(thing_id: 1) }
130
+ let(:path) { "/things/1" }
131
+ it_behaves_like "an association with a path"
132
+ end
133
+
134
+ context "with the :path option" do
135
+ subject { BelongsToWithPath.new }
136
+ let(:path) { "/custom/thing" }
137
+ it_behaves_like "an association with a path"
138
+ end
139
+
140
+ context "with :path variables" do
141
+ subject { BelongsToWithPathVariables.new(name: "stuff") }
142
+ let(:path) { "/custom/stuff/thing" }
143
+ it_behaves_like "an association with a path"
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,57 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Remotely::Collection do
4
+ let(:finn) { Member.new(id: 1, name: "Finn", type: "human") }
5
+ let(:jake) { Member.new(id: 2, name: "Jake", type: "dog") }
6
+ let(:adventure) { Adventure.new(id: 3) }
7
+
8
+ subject { Remotely::Collection.new(adventure, Member, [jake, finn]) }
9
+
10
+ describe "#find" do
11
+ it "finds by id" do
12
+ subject.find(1).should == finn
13
+ end
14
+ end
15
+
16
+ describe "#where" do
17
+ it "is searchable by attributes and values" do
18
+ subject.where(name: "Jake", type: "dog").should == [jake]
19
+ end
20
+
21
+ it "returns a new Collection" do
22
+ subject.where(name: "Jake", type: "dog").should be_a Remotely::Collection
23
+ end
24
+ end
25
+
26
+ describe "#order" do
27
+ it "orders by an attribute" do
28
+ subject.order(:name).should == [finn, jake]
29
+ end
30
+ end
31
+
32
+ describe "#build" do
33
+ it "creates a new model object with the foreign key automatically defined" do
34
+ adventure.members.build.adventure_id.should == 3
35
+ end
36
+
37
+ it "adds the new object to itself" do
38
+ new_member = adventure.members.build
39
+ adventure.members.should include(new_member)
40
+ end
41
+ end
42
+
43
+ describe "#create" do
44
+ before do
45
+ stub_request(:post, %r(/members)).to_return(lambda { |req| {body: req.body} })
46
+ end
47
+
48
+ it "creates and saves a new model object with the foreign key automatically defined" do
49
+ adventure.members.create.adventure_id.should == 3
50
+ end
51
+
52
+ it "adds the new object to itself" do
53
+ new_member = adventure.members.create
54
+ adventure.members.should include(new_member)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ require "spec_helper"
2
+
3
+ describe URL do
4
+ it "takes n number of arguments and joins them" do
5
+ URL.new("a", "b", "c").should == "/a/b/c"
6
+ end
7
+
8
+ it "removes duplicate slashes" do
9
+ URL.new("a", "/", "b").should == "/a/b"
10
+ end
11
+
12
+ it "is comparable" do
13
+ URL.new("a", "b").should == URL.new("a", "b")
14
+ end
15
+
16
+ it "is addable" do
17
+ (URL.new("a", "b") + URL.new("c")).to_s.should == "/a/b/c"
18
+ end
19
+
20
+ it "is subtractable" do
21
+ (URL.new("a", "b") - URL.new("b")).to_s.should == "/a"
22
+ end
23
+
24
+ it "creatable using URL()" do
25
+ URL("a", "b").should == "/a/b"
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ require "spec_helper"
2
+
3
+ describe Remotely::HTTPMethods do
4
+ include Remotely::HTTPMethods
5
+
6
+ it "raises NonJsonResponseError when HTML is returned on GET" do
7
+ stub_request(:get, %r(/things)).to_return(body: "<html><head><title></title></head></html>")
8
+ expect { get("/things") }.to raise_error(Remotely::NonJsonResponseError)
9
+ end
10
+
11
+ it "raises NonJsonResponseError when HTML is returned on POST" do
12
+ stub_request(:post, %r(/things)).to_return(body: "<html><head><title></title></head></html>")
13
+ expect { post("/things") }.to raise_error(Remotely::NonJsonResponseError)
14
+ end
15
+
16
+ it "raises NonJsonResponseError when HTML is returned on PUT" do
17
+ stub_request(:put, %r(/things)).to_return(body: "<html><head><title></title></head></html>")
18
+ expect { put("/things") }.to raise_error(Remotely::NonJsonResponseError)
19
+ end
20
+
21
+ it "raises NonJsonResponseError when HTML is returned on DELETE" do
22
+ stub_request(:delete, %r(/things)).to_return(body: "<html><head><title></title></head></html>")
23
+ expect { http_delete("/things") }.to raise_error(Remotely::NonJsonResponseError)
24
+ end
25
+ end
@@ -0,0 +1,368 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Remotely::Model do
4
+ let(:app) { "http://localhost:1234" }
5
+ let(:attributes) { {id: 1, name: "Marceline Quest", type: "MATHEMATICAL!"} }
6
+
7
+ subject { Adventure.new(attributes) }
8
+
9
+ describe ".attr_savable" do
10
+ let(:attrs) { {id: 2, name: "Wishes!", type: "MATHEMATICAL!", length: 9} }
11
+ let(:saved) { to_json({name: "OMG New Name!", type: "MATHEMATICAL!", id: 2}) }
12
+
13
+ subject { Adventure.new(attrs) }
14
+
15
+ it "stores which attributes are savable" do
16
+ Adventure.savable_attributes.should == [:name, :type]
17
+ end
18
+
19
+ it "only sends the specified attributes when saving an existing record" do
20
+ stub_request(:put, "#{app}/adventures/2").to_return(body: saved)
21
+ subject.update_attribute(:name, "OMG New Name!")
22
+ a_request(:put, "#{app}/adventures/2").with(body: saved).should have_been_made
23
+ end
24
+ end
25
+
26
+ describe ".find" do
27
+ it "retreives an individual resource" do
28
+ Adventure.find(1)
29
+ a_request(:get, "#{app}/adventures/1").should have_been_made
30
+ end
31
+ end
32
+
33
+ describe ".where" do
34
+ it "searches for resources" do
35
+ Adventure.where(:type => "MATHEMATICAL!")
36
+ a_request(:get, "#{app}/adventures/search?type=MATHEMATICAL!").should have_been_made
37
+ end
38
+
39
+ it "returns a collection of resources" do
40
+ Adventure.where(:type => "MATHEMATICAL!").should be_a Remotely::Collection
41
+ end
42
+ end
43
+
44
+ describe ".destroy" do
45
+ it "destroys the resource" do
46
+ Adventure.destroy(1)
47
+ a_request(:delete, "#{app}/adventures/1").should have_been_made
48
+ end
49
+
50
+ it "returns true on success" do
51
+ Adventure.destroy(1).should be_true
52
+ end
53
+
54
+ it "returns false on failure" do
55
+ stub_request(:delete, %r[/adventures/1]).to_return(status: 500)
56
+ Adventure.destroy(1).should be_false
57
+ end
58
+ end
59
+
60
+ describe ".create" do
61
+ let(:attrs) { attributes.except(:id) }
62
+
63
+ before do
64
+ stub_request(:post, "#{app}/adventures").to_return(lambda { |req| { body: req.body, status: 201 }})
65
+ end
66
+
67
+ it "creates the resource" do
68
+ Adventure.create(attrs)
69
+ a_request(:post, "#{app}/adventures").with(attrs).should have_been_made
70
+ end
71
+
72
+ it "returns the new resource on creation" do
73
+ Adventure.create(attrs).name.should == "Marceline Quest"
74
+ end
75
+
76
+ it "returns an instance with errors when the creation fails" do
77
+ body = Yajl.dump({errors: {base: ["error"]}})
78
+ stub_request(:post, %r[/adventures]).to_return(status: 500, body: body)
79
+ Adventure.create(attrs).errors[:base].should include("error")
80
+ end
81
+ end
82
+
83
+ describe ".find_or_" do
84
+ let(:body) { Yajl::Encoder.encode([{id: 1, name: "BubbleGum"}]) }
85
+ let(:stub_success) { stub_request(:get, "#{app}/adventures/search?name=BubbleGum").to_return(body: body) }
86
+ let(:stub_failure) { stub_request(:get, "#{app}/adventures/search?name=BubbleGum").to_return(body: "[]") }
87
+
88
+ describe "initialize" do
89
+ it "tries to fetch the record" do
90
+ stub_success
91
+ Adventure.find_or_initialize(name: "BubbleGum")
92
+ a_request(:get, "#{app}/adventures/search?name=BubbleGum").should have_been_made
93
+ end
94
+
95
+ it "returns the fetched object if found" do
96
+ stub_success
97
+ Adventure.find_or_initialize(name: "BubbleGum").id.should == 1
98
+ end
99
+
100
+ it "creates a new object if one is not found" do
101
+ stub_failure
102
+ Adventure.find_or_initialize(name: "BubbleGum").should be_a_new_record
103
+ end
104
+ end
105
+
106
+ describe "create" do
107
+ it "automatically saves the new object" do
108
+ stub_failure
109
+ Adventure.should_receive(:create).with(name: "BubbleGum")
110
+ Adventure.find_or_create(name: "BubbleGum")
111
+ end
112
+
113
+ it "returns the first item from the collection" do
114
+ stub_success
115
+ Adventure.find_or_create(name: "BubbleGum").should be_an Adventure
116
+ end
117
+ end
118
+ end
119
+
120
+ describe ".find_by_*" do
121
+ it "searches by a single attribute" do
122
+ Adventure.find_by_name("Fun")
123
+ a_request(:get, "#{app}/adventures/search?name=Fun").should have_been_made
124
+ end
125
+
126
+ it "searches by multiple attributes seperated by 'and'" do
127
+ Adventure.find_by_name_and_type("Fun", "MATHEMATICAL!")
128
+ a_request(:get, "#{app}/adventures/search?name=Fun&type=MATHEMATICAL!").should have_been_made
129
+ end
130
+ end
131
+
132
+ describe ".all" do
133
+ it "fetches all resources" do
134
+ Adventure.all
135
+ a_request(:get, "#{app}/adventures").should have_been_made
136
+ end
137
+ end
138
+
139
+ describe ".update_all" do
140
+ it "request an update to all entries" do
141
+ Adventure.update_all(type: "awesome")
142
+ a_request(:put, "#{app}/adventures").with(type: "awesome").should have_been_made
143
+ end
144
+ end
145
+
146
+ describe "#save" do
147
+ let(:new_name) { "City of Thieves" }
148
+ let(:new_attributes) { attributes.merge(name: new_name) }
149
+
150
+ context "when updating" do
151
+ it "updates the resource" do
152
+ adventure = Adventure.new(attributes)
153
+ adventure.name = new_name
154
+ adventure.save
155
+ a_request(:put, "#{app}/adventures/1").with(new_attributes).should have_been_made
156
+ end
157
+
158
+ it "returns true when the save succeeds" do
159
+ Adventure.new(attributes).save.should be_a Adventure
160
+ end
161
+
162
+ it "sets errors when a save fails" do
163
+ adventure = Adventure.new(attributes)
164
+ stub_request(:put, %r[/adventures/1]).to_return(status: 409, body: to_json({errors: {base: %w{this failed}}}))
165
+ adventure.save
166
+ adventure.errors[:base].should == %w{this failed}
167
+ end
168
+ end
169
+
170
+ context "when creating" do
171
+ it "merges in the response body to attributes on success" do
172
+ adventure = Adventure.new(name: "To Be Saved...")
173
+ stub_request(:post, %r(/adventures)).to_return(body: to_json(attributes.merge(name: "To Be Saved...", id: 2)), status: 201)
174
+ adventure.save
175
+ adventure.id.should == 2
176
+ end
177
+
178
+ it "sets errors on a failure" do
179
+ body = Yajl.dump({errors: {base: ["error"]}})
180
+ stub_request(:post, %r(/adventures)).to_return(status: 409, body: body)
181
+ Adventure.new(name: "name").save.errors.should_not be_empty
182
+ end
183
+ end
184
+ end
185
+
186
+ describe "#update_attribute" do
187
+ it "updates a single attribute and saves" do
188
+ subject.update_attribute(:type, "powerful")
189
+ a_request(:put, "#{app}/adventures/1").with(type: "powerful").should have_been_made
190
+ end
191
+ end
192
+
193
+ describe "#to_param" do
194
+ it "returns correct value" do
195
+ subject.to_param.should == '1'
196
+ end
197
+ end
198
+
199
+ describe "#update_attributes" do
200
+ let(:updates) { {type: "awesome"} }
201
+ let(:new_attributes) { subject.attributes.merge(updates) }
202
+
203
+ it "replaces existing attribute values" do
204
+ subject.update_attributes(updates)
205
+ subject.type.should == "awesome"
206
+ end
207
+
208
+ it "calls save" do
209
+ subject.update_attributes(updates)
210
+ a_request(:put, "#{app}/adventures/1").with(new_attributes).should have_been_made
211
+ end
212
+
213
+ it "returns true on success" do
214
+ subject.update_attributes(updates).should be_true
215
+ end
216
+
217
+ it "sets errors on failure" do
218
+ body = Yajl.dump({errors: {base: ["error"]}})
219
+ stub_request(:put, %r[/adventures/1]).to_return(status: 500, body: body)
220
+ subject.update_attributes(updates)
221
+ subject.errors[:base].should include("error")
222
+ end
223
+
224
+ it "reverts the object's attributes if the save fails" do
225
+ body = Yajl.dump({errors: {base: ["error"]}})
226
+ stub_request(:put, %r[/adventures/1]).to_return(status: 500, body: body)
227
+ subject.update_attributes(updates)
228
+ subject.type.should == "MATHEMATICAL!"
229
+ end
230
+ end
231
+
232
+ describe "#destroy" do
233
+ it "destroys a resource with the might of 60 jotun!!" do
234
+ Adventure.new(attributes).destroy
235
+ a_request(:delete, "#{app}/adventures/1").should have_been_made
236
+ end
237
+ end
238
+
239
+ describe "associations" do
240
+ let(:member) { Member.new(id: 2, name_id: 1) }
241
+
242
+ it "creates associations when instantiated" do
243
+ member.should respond_to :name
244
+ end
245
+
246
+ it "fetches the resource when accessed" do
247
+ Name.should_receive(:find).with(1)
248
+ member.name
249
+ end
250
+
251
+ it "doesn't fetch a resource twice" do
252
+ Name.should_receive(:find).with(1).once
253
+ member.name
254
+ member.name
255
+ end
256
+
257
+ it "reloads association objects" do
258
+ Name.should_receive(:find).with(1).twice
259
+ member.name
260
+ member.name(true)
261
+ end
262
+ end
263
+
264
+ context "basic auth" do
265
+ before do
266
+ Remotely.configure { app :adventure_app, "http://localhost:3000" }
267
+ end
268
+
269
+ after do
270
+ Remotely.reset!
271
+ end
272
+
273
+ it "sends Authorization headers when basic auth is configured" do
274
+ Remotely.configure { basic_auth "user", "password" }
275
+ Adventure.find(1)
276
+ a_request(:get, "#{app}/adventures/1").with(headers: {'Authorization' => "Basic dXNlcjpwYXNzd29yZA=="})
277
+ end
278
+
279
+ it "doesn't send Authorization headers when basic auth is not configured" do
280
+ Adventure.find(1)
281
+ a_request(:get, "#{app}/adventures/1").with(headers: {})
282
+ end
283
+ end
284
+
285
+ it "sets the app it belongs to" do
286
+ Adventure.app.should == :adventure_app
287
+ end
288
+
289
+ it "sets the uri to itself" do
290
+ Adventure.uri.should == "/adventures"
291
+ end
292
+
293
+ it "has a connection" do
294
+ Adventure.remotely_connection.should be_a Faraday::Connection
295
+ end
296
+
297
+ it "supports ActiveModel::Naming methods" do
298
+ Adventure.model_name.element.should == "adventure"
299
+ end
300
+
301
+ it "is reloadable" do
302
+ subject.reload
303
+ a_request(:get, "#{app}/adventures/1")
304
+ end
305
+
306
+ it "symbolizes attribute keys" do
307
+ subject.attributes.should == attributes
308
+ end
309
+
310
+ it "can be initialized with a hash of attribute/values" do
311
+ subject.name.should == "Marceline Quest"
312
+ end
313
+
314
+ it "sets an attribute value" do
315
+ subject.name = "City of Thieves"
316
+ subject.name.should == "City of Thieves"
317
+ end
318
+
319
+ it "raises a normal NoMethodError for non-existent attributes" do
320
+ expect { subject.height }.to raise_error(NoMethodError)
321
+ end
322
+
323
+ it "is a new_record when no id exists" do
324
+ subject.id = nil
325
+ subject.should be_a_new_record
326
+ end
327
+
328
+ it "creates boolean methods for each attribute" do
329
+ subject.name?.should == true
330
+ end
331
+
332
+ it "returns id from #to_key" do
333
+ subject.id = 1
334
+ subject.to_key.should == [1]
335
+ end
336
+
337
+ it "returns id from #to_param" do
338
+ subject.id = 1
339
+ subject.to_param.should == "1"
340
+ end
341
+
342
+ it "returns itself from #to_model" do
343
+ subject.to_model.should == subject
344
+ end
345
+
346
+ context "with an app uri" do
347
+ before do
348
+ Remotely.app :uri_app, "http://localhost:3000/api"
349
+ Thing.app :uri_app
350
+ end
351
+
352
+ it "prepends the app uri" do
353
+ Thing.expand("/members").should == "/api/members"
354
+ end
355
+
356
+ it "doesn't prepend when it's already there" do
357
+ Thing.expand("/api/members").should == "/api/members"
358
+ end
359
+ end
360
+
361
+ context "with errors" do
362
+ let(:attributes) { {'errors' => {:base => %w{totally failed dude}}} }
363
+
364
+ it "adds errors during #initialize" do
365
+ subject.errors[:base].should == %w{totally failed dude}
366
+ end
367
+ end
368
+ end