herr 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +15 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +990 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +81 -0
  12. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  13. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  16. data/her.gemspec +30 -0
  17. data/lib/her.rb +16 -0
  18. data/lib/her/api.rb +115 -0
  19. data/lib/her/collection.rb +12 -0
  20. data/lib/her/errors.rb +27 -0
  21. data/lib/her/middleware.rb +10 -0
  22. data/lib/her/middleware/accept_json.rb +17 -0
  23. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  24. data/lib/her/middleware/parse_json.rb +21 -0
  25. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  26. data/lib/her/model.rb +72 -0
  27. data/lib/her/model/associations.rb +141 -0
  28. data/lib/her/model/associations/association.rb +103 -0
  29. data/lib/her/model/associations/association_proxy.rb +46 -0
  30. data/lib/her/model/associations/belongs_to_association.rb +96 -0
  31. data/lib/her/model/associations/has_many_association.rb +100 -0
  32. data/lib/her/model/associations/has_one_association.rb +79 -0
  33. data/lib/her/model/attributes.rb +266 -0
  34. data/lib/her/model/base.rb +33 -0
  35. data/lib/her/model/deprecated_methods.rb +61 -0
  36. data/lib/her/model/http.rb +114 -0
  37. data/lib/her/model/introspection.rb +65 -0
  38. data/lib/her/model/nested_attributes.rb +45 -0
  39. data/lib/her/model/orm.rb +205 -0
  40. data/lib/her/model/parse.rb +227 -0
  41. data/lib/her/model/paths.rb +121 -0
  42. data/lib/her/model/relation.rb +164 -0
  43. data/lib/her/version.rb +3 -0
  44. data/spec/api_spec.rb +131 -0
  45. data/spec/collection_spec.rb +26 -0
  46. data/spec/middleware/accept_json_spec.rb +10 -0
  47. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  48. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  49. data/spec/model/associations_spec.rb +416 -0
  50. data/spec/model/attributes_spec.rb +268 -0
  51. data/spec/model/callbacks_spec.rb +145 -0
  52. data/spec/model/dirty_spec.rb +86 -0
  53. data/spec/model/http_spec.rb +194 -0
  54. data/spec/model/introspection_spec.rb +76 -0
  55. data/spec/model/nested_attributes_spec.rb +134 -0
  56. data/spec/model/orm_spec.rb +479 -0
  57. data/spec/model/parse_spec.rb +373 -0
  58. data/spec/model/paths_spec.rb +341 -0
  59. data/spec/model/relation_spec.rb +226 -0
  60. data/spec/model/validations_spec.rb +42 -0
  61. data/spec/model_spec.rb +31 -0
  62. data/spec/spec_helper.rb +26 -0
  63. data/spec/support/extensions/array.rb +5 -0
  64. data/spec/support/extensions/hash.rb +5 -0
  65. data/spec/support/macros/her_macros.rb +17 -0
  66. data/spec/support/macros/model_macros.rb +29 -0
  67. data/spec/support/macros/request_macros.rb +27 -0
  68. metadata +280 -0
@@ -0,0 +1,194 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::HTTP do
5
+ context "binding a model with an API" do
6
+ let(:api1) { Her::API.new :url => "https://api1.example.com" }
7
+ let(:api2) { Her::API.new :url => "https://api2.example.com" }
8
+
9
+ before do
10
+ spawn_model("Foo::User")
11
+ spawn_model("Foo::Comment")
12
+ Her::API.setup :url => "https://api.example.com"
13
+ end
14
+
15
+ context "when binding a model to an instance of Her::API" do
16
+ before { Foo::User.uses_api api1 }
17
+ subject { Foo::User.her_api }
18
+ its(:base_uri) { should == "https://api1.example.com" }
19
+ end
20
+
21
+ context "when binding a model directly to Her::API" do
22
+ before { spawn_model "Foo::User" }
23
+ subject { Foo::User.her_api }
24
+ its(:base_uri) { should == "https://api.example.com" }
25
+ end
26
+
27
+ context "when using a proc for uses_api" do
28
+ before do
29
+ Foo::User.uses_api lambda { Her::API.new :url => 'http://api-lambda.example.com' }
30
+ end
31
+
32
+ specify { Foo::User.her_api.base_uri.should == 'http://api-lambda.example.com' }
33
+ end
34
+
35
+ context "when binding two models to two different instances of Her::API" do
36
+ before do
37
+ Foo::User.uses_api api1
38
+ Foo::Comment.uses_api api2
39
+ end
40
+
41
+ specify { Foo::User.her_api.base_uri.should == "https://api1.example.com" }
42
+ specify { Foo::Comment.her_api.base_uri.should == "https://api2.example.com" }
43
+ end
44
+
45
+ context "binding one model to Her::API and another one to an instance of Her::API" do
46
+ before { Foo::Comment.uses_api api2 }
47
+ specify { Foo::User.her_api.base_uri.should == "https://api.example.com" }
48
+ specify { Foo::Comment.her_api.base_uri.should == "https://api2.example.com" }
49
+ end
50
+
51
+ context "when binding a model to its superclass' her_api" do
52
+ before do
53
+ spawn_model "Foo::Superclass"
54
+ Foo::Superclass.uses_api api1
55
+ Foo::Subclass = Class.new(Foo::Superclass)
56
+ end
57
+
58
+ specify { Foo::Subclass.her_api.should == Foo::Superclass.her_api }
59
+ end
60
+
61
+ context "when changing her_api without changing the parent class' her_api" do
62
+ before do
63
+ spawn_model "Foo::Superclass"
64
+ Foo::Subclass = Class.new(Foo::Superclass)
65
+ Foo::Superclass.uses_api api1
66
+ Foo::Subclass.uses_api api2
67
+ end
68
+
69
+ specify { Foo::Subclass.her_api.should_not == Foo::Superclass.her_api }
70
+ end
71
+ end
72
+
73
+ context "making HTTP requests" do
74
+ before do
75
+ Her::API.setup :url => "https://api.example.com" do |builder|
76
+ builder.use Her::Middleware::FirstLevelParseJSON
77
+ builder.use Faraday::Request::UrlEncoded
78
+ builder.adapter :test do |stub|
79
+ stub.get("/users") { |env| [200, {}, [{ :id => 1 }].to_json] }
80
+ stub.get("/users/1") { |env| [200, {}, { :id => 1 }.to_json] }
81
+ stub.get("/users/popular") do |env|
82
+ if env[:params]["page"] == "2"
83
+ [200, {}, [{ :id => 3 }, { :id => 4 }].to_json]
84
+ else
85
+ [200, {}, [{ :id => 1 }, { :id => 2 }].to_json]
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ spawn_model "Foo::User"
92
+ end
93
+
94
+ describe :get do
95
+ subject { Foo::User.get(:popular) }
96
+ its(:length) { should == 2 }
97
+ specify { subject.first.id.should == 1 }
98
+ end
99
+
100
+ describe :get_raw do
101
+ context "with a block" do
102
+ specify do
103
+ Foo::User.get_raw("/users") do |parsed_data, response|
104
+ parsed_data[:data].should == [{ :id => 1 }]
105
+ end
106
+ end
107
+ end
108
+
109
+ context "with a return value" do
110
+ subject { Foo::User.get_raw("/users") }
111
+ specify { subject[:parsed_data][:data].should == [{ :id => 1 }] }
112
+ end
113
+ end
114
+
115
+ describe :get_collection do
116
+ context "with a String path" do
117
+ subject { Foo::User.get_collection("/users/popular") }
118
+ its(:length) { should == 2 }
119
+ specify { subject.first.id.should == 1 }
120
+ end
121
+
122
+ context "with a Symbol" do
123
+ subject { Foo::User.get_collection(:popular) }
124
+ its(:length) { should == 2 }
125
+ specify { subject.first.id.should == 1 }
126
+ end
127
+
128
+ context "with extra parameters" do
129
+ subject { Foo::User.get_collection(:popular, :page => 2) }
130
+ its(:length) { should == 2 }
131
+ specify { subject.first.id.should == 3 }
132
+ end
133
+ end
134
+
135
+ describe :get_resource do
136
+ context "with a String path" do
137
+ subject { Foo::User.get_resource("/users/1") }
138
+ its(:id) { should == 1 }
139
+ end
140
+
141
+ context "with a Symbol" do
142
+ subject { Foo::User.get_resource(:"1") }
143
+ its(:id) { should == 1 }
144
+ end
145
+ end
146
+
147
+ describe :get_raw do
148
+ specify do
149
+ Foo::User.get_raw(:popular) do |parsed_data, response|
150
+ parsed_data[:data].should == [{ :id => 1 }, { :id => 2 }]
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ context "setting custom HTTP requests" do
157
+ before do
158
+ Her::API.setup :url => "https://api.example.com" do |connection|
159
+ connection.use Her::Middleware::FirstLevelParseJSON
160
+ connection.adapter :test do |stub|
161
+ stub.get("/users/popular") { |env| [200, {}, [{ :id => 1 }, { :id => 2 }].to_json] }
162
+ stub.post("/users/from_default") { |env| [200, {}, { :id => 4 }.to_json] }
163
+ end
164
+ end
165
+
166
+ spawn_model "Foo::User"
167
+ end
168
+
169
+ subject { Foo::User }
170
+
171
+ describe :custom_get do
172
+ context "without cache" do
173
+ before { Foo::User.custom_get :popular, :recent }
174
+ it { should respond_to(:popular) }
175
+ it { should respond_to(:recent) }
176
+
177
+ context "making the HTTP request" do
178
+ subject { Foo::User.popular }
179
+ its(:length) { should == 2 }
180
+ end
181
+ end
182
+ end
183
+
184
+ describe :custom_post do
185
+ before { Foo::User.custom_post :from_default }
186
+ it { should respond_to(:from_default) }
187
+
188
+ context "making the HTTP request" do
189
+ subject { Foo::User.from_default(:name => "Tobias Fünke") }
190
+ its(:id) { should == 4 }
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::Introspection do
5
+ context "introspecting a resource" do
6
+ before do
7
+ Her::API.setup :url => "https://api.example.com" do |builder|
8
+ builder.use Her::Middleware::FirstLevelParseJSON
9
+ builder.use Faraday::Request::UrlEncoded
10
+ builder.adapter :test do |stub|
11
+ stub.post("/users") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
12
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
13
+ stub.put("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
14
+ stub.delete("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
15
+ stub.get("/projects/1/comments") { |env| [200, {}, [{ :id => 1, :body => "Hello!" }].to_json] }
16
+ end
17
+ end
18
+
19
+ spawn_model "Foo::User"
20
+ spawn_model "Foo::Comment" do
21
+ collection_path "projects/:project_id/comments"
22
+ end
23
+ end
24
+
25
+ describe "#inspect" do
26
+ it "outputs resource attributes for an existing resource" do
27
+ @user = Foo::User.find(1)
28
+ ["#<Foo::User(users/1) name=\"Tobias Funke\" id=1>", "#<Foo::User(users/1) id=1 name=\"Tobias Funke\">"].should include(@user.inspect)
29
+ end
30
+
31
+ it "outputs resource attributes for an not-saved-yet resource" do
32
+ @user = Foo::User.new(:name => "Tobias Funke")
33
+ @user.inspect.should == "#<Foo::User(users) name=\"Tobias Funke\">"
34
+ end
35
+
36
+ it "outputs resource attributes using getters" do
37
+ @user = Foo::User.new(:name => "Tobias Funke", :password => "Funke")
38
+ @user.instance_eval {def password; 'filtered'; end}
39
+ @user.inspect.should include("name=\"Tobias Funke\"")
40
+ @user.inspect.should include("password=\"filtered\"")
41
+ @user.inspect.should_not include("password=\"Funke\"")
42
+ end
43
+
44
+ it "support dash on attribute" do
45
+ @user = Foo::User.new(:'life-span' => "3 years")
46
+ @user.inspect.should include("life-span=\"3 years\"")
47
+ end
48
+ end
49
+
50
+ describe "#inspect with errors in resource path" do
51
+ it "prints the resource path as “unknown”" do
52
+ @comment = Foo::Comment.where(:project_id => 1).first
53
+ path = '<unknown path, missing `project_id`>'
54
+ ["#<Foo::Comment(#{path}) body=\"Hello!\" id=1>", "#<Foo::Comment(#{path}) id=1 body=\"Hello!\">"].should include(@comment.inspect)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "#her_nearby_class" do
60
+ context "for a class inside of a module" do
61
+ before do
62
+ spawn_model "Foo::User"
63
+ spawn_model "Foo::AccessRecord"
64
+ spawn_model "AccessRecord"
65
+ spawn_model "Log"
66
+ end
67
+
68
+ it "returns a sibling class, if found" do
69
+ Foo::User.her_nearby_class("AccessRecord").should == Foo::AccessRecord
70
+ AccessRecord.her_nearby_class("Log").should == Log
71
+ Foo::User.her_nearby_class("Log").should == Log
72
+ Foo::User.her_nearby_class("X").should be_nil
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,134 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::NestedAttributes do
5
+ context "with a belongs_to association" do
6
+ before do
7
+ Her::API.setup :url => "https://api.example.com" do |builder|
8
+ builder.use Her::Middleware::FirstLevelParseJSON
9
+ builder.use Faraday::Request::UrlEncoded
10
+ end
11
+
12
+ spawn_model "Foo::User" do
13
+ belongs_to :company, :path => "/organizations/:id", :foreign_key => :organization_id
14
+ accepts_nested_attributes_for :company
15
+ end
16
+
17
+ spawn_model "Foo::Company"
18
+
19
+ @user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :company_attributes => { :name => "Example Company" }
20
+ end
21
+
22
+ context "when child does not yet exist" do
23
+ it "creates an instance of the associated class" do
24
+ @user_with_data_through_nested_attributes.company.should be_a(Foo::Company)
25
+ @user_with_data_through_nested_attributes.company.name.should == "Example Company"
26
+ end
27
+ end
28
+
29
+ context "when child does exist" do
30
+ it "updates the attributes of the associated object" do
31
+ @user_with_data_through_nested_attributes.company_attributes = { :name => "Fünke's Company" }
32
+ @user_with_data_through_nested_attributes.company.should be_a(Foo::Company)
33
+ @user_with_data_through_nested_attributes.company.name.should == "Fünke's Company"
34
+ end
35
+ end
36
+ end
37
+
38
+ context "with a has_one association" do
39
+ before do
40
+ Her::API.setup :url => "https://api.example.com" do |builder|
41
+ builder.use Her::Middleware::FirstLevelParseJSON
42
+ builder.use Faraday::Request::UrlEncoded
43
+ end
44
+
45
+ spawn_model "Foo::User" do
46
+ has_one :pet
47
+ accepts_nested_attributes_for :pet
48
+ end
49
+
50
+ spawn_model "Foo::Pet"
51
+
52
+ @user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :pet_attributes => { :name => "Hasi" }
53
+ end
54
+
55
+ context "when child does not yet exist" do
56
+ it "creates an instance of the associated class" do
57
+ @user_with_data_through_nested_attributes.pet.should be_a(Foo::Pet)
58
+ @user_with_data_through_nested_attributes.pet.name.should == "Hasi"
59
+ end
60
+ end
61
+
62
+ context "when child does exist" do
63
+ it "updates the attributes of the associated object" do
64
+ @user_with_data_through_nested_attributes.pet_attributes = { :name => "Rodriguez" }
65
+ @user_with_data_through_nested_attributes.pet.should be_a(Foo::Pet)
66
+ @user_with_data_through_nested_attributes.pet.name.should == "Rodriguez"
67
+ end
68
+ end
69
+ end
70
+
71
+ context "with a has_many association" do
72
+ before do
73
+ Her::API.setup :url => "https://api.example.com" do |builder|
74
+ builder.use Her::Middleware::FirstLevelParseJSON
75
+ builder.use Faraday::Request::UrlEncoded
76
+ end
77
+
78
+ spawn_model "Foo::User" do
79
+ has_many :pets
80
+ accepts_nested_attributes_for :pets
81
+ end
82
+
83
+ spawn_model "Foo::Pet"
84
+
85
+ @user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :pets_attributes => [{ :name => "Hasi" }, { :name => "Rodriguez" }]
86
+ end
87
+
88
+ context "when children do not yet exist" do
89
+ it "creates an instance of the associated class" do
90
+ @user_with_data_through_nested_attributes.pets.length.should == 2
91
+ @user_with_data_through_nested_attributes.pets[0].should be_a(Foo::Pet)
92
+ @user_with_data_through_nested_attributes.pets[1].should be_a(Foo::Pet)
93
+ @user_with_data_through_nested_attributes.pets[0].name.should == "Hasi"
94
+ @user_with_data_through_nested_attributes.pets[1].name.should == "Rodriguez"
95
+ end
96
+ end
97
+ end
98
+
99
+ context "with a has_many association as a Hash" do
100
+ before do
101
+ Her::API.setup :url => "https://api.example.com" do |builder|
102
+ builder.use Her::Middleware::FirstLevelParseJSON
103
+ builder.use Faraday::Request::UrlEncoded
104
+ end
105
+
106
+ spawn_model "Foo::User" do
107
+ has_many :pets
108
+ accepts_nested_attributes_for :pets
109
+ end
110
+
111
+ spawn_model "Foo::Pet"
112
+
113
+ @user_with_data_through_nested_attributes_as_hash = Foo::User.new :name => "Test", :pets_attributes => { '0' => { :name => "Hasi" }, '1' => { :name => "Rodriguez" }}
114
+ end
115
+
116
+ context "when children do not yet exist" do
117
+ it "creates an instance of the associated class" do
118
+ @user_with_data_through_nested_attributes_as_hash.pets.length.should == 2
119
+ @user_with_data_through_nested_attributes_as_hash.pets[0].should be_a(Foo::Pet)
120
+ @user_with_data_through_nested_attributes_as_hash.pets[1].should be_a(Foo::Pet)
121
+ @user_with_data_through_nested_attributes_as_hash.pets[0].name.should == "Hasi"
122
+ @user_with_data_through_nested_attributes_as_hash.pets[1].name.should == "Rodriguez"
123
+ end
124
+ end
125
+ end
126
+
127
+ context "with an unknown association" do
128
+ it "raises an error" do
129
+ expect {
130
+ spawn_model("Foo::User") { accepts_nested_attributes_for :company }
131
+ }.to raise_error(Her::Errors::AssociationUnknownError, 'Unknown association name :company')
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,479 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::ORM do
5
+ context "mapping data to Ruby objects" do
6
+ before do
7
+ api = Her::API.new
8
+ api.setup :url => "https://api.example.com" do |builder|
9
+ builder.use Her::Middleware::FirstLevelParseJSON
10
+ builder.use Faraday::Request::UrlEncoded
11
+ builder.adapter :test do |stub|
12
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke" }.to_json] }
13
+ stub.get("/users") { |env| [200, {}, [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }].to_json] }
14
+ stub.get("/admin_users") { |env| [200, {}, [{ :admin_id => 1, :name => "Tobias Fünke" }, { :admin_id => 2, :name => "Lindsay Fünke" }].to_json] }
15
+ stub.get("/admin_users/1") { |env| [200, {}, { :admin_id => 1, :name => "Tobias Fünke" }.to_json] }
16
+ end
17
+ end
18
+
19
+ spawn_model "Foo::User" do
20
+ uses_api api
21
+ end
22
+
23
+ spawn_model "Foo::AdminUser" do
24
+ uses_api api
25
+ primary_key :admin_id
26
+ end
27
+ end
28
+
29
+ it "maps a single resource to a Ruby object" do
30
+ @user = Foo::User.find(1)
31
+ @user.id.should == 1
32
+ @user.name.should == "Tobias Fünke"
33
+
34
+ @admin = Foo::AdminUser.find(1)
35
+ @admin.id.should == 1
36
+ @admin.name.should == "Tobias Fünke"
37
+ end
38
+
39
+ it "maps a collection of resources to an array of Ruby objects" do
40
+ @users = Foo::User.all
41
+ @users.length.should == 2
42
+ @users.first.name.should == "Tobias Fünke"
43
+
44
+ @users = Foo::AdminUser.all
45
+ @users.length.should == 2
46
+ @users.first.name.should == "Tobias Fünke"
47
+ end
48
+
49
+ it "handles new resource" do
50
+ @new_user = Foo::User.new(:fullname => "Tobias Fünke")
51
+ @new_user.new?.should be_truthy
52
+ @new_user.fullname.should == "Tobias Fünke"
53
+
54
+ @existing_user = Foo::User.find(1)
55
+ @existing_user.new?.should be_falsey
56
+ end
57
+
58
+ it 'handles new resource with custom primary key' do
59
+ @new_user = Foo::AdminUser.new(:fullname => 'Lindsay Fünke', :id => -1)
60
+ @new_user.should be_new
61
+
62
+ @existing_user = Foo::AdminUser.find(1)
63
+ @existing_user.should_not be_new
64
+ end
65
+ end
66
+
67
+ context "mapping data, metadata and error data to Ruby objects" do
68
+ before do
69
+ api = Her::API.new
70
+ api.setup :url => "https://api.example.com" do |builder|
71
+ builder.use Her::Middleware::SecondLevelParseJSON
72
+ builder.use Faraday::Request::UrlEncoded
73
+ builder.adapter :test do |stub|
74
+ stub.get("/users") { |env| [200, {}, { :data => [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }], :metadata => { :total_pages => 10, :next_page => 2 }, :errors => ["Oh", "My", "God"] }.to_json] }
75
+ stub.post("/users") { |env| [200, {}, { :data => { :name => "George Michael Bluth" }, :metadata => { :foo => "bar" }, :errors => ["Yes", "Sir"] }.to_json] }
76
+ end
77
+ end
78
+
79
+ spawn_model :User do
80
+ uses_api api
81
+ end
82
+ end
83
+
84
+ it "handles metadata on a collection" do
85
+ @users = User.all
86
+ @users.metadata[:total_pages].should == 10
87
+ end
88
+
89
+ it "handles error data on a collection" do
90
+ @users = User.all
91
+ @users.errors.length.should == 3
92
+ end
93
+
94
+ it "handles metadata on a resource" do
95
+ @user = User.create(:name => "George Michael Bluth")
96
+ @user.metadata[:foo].should == "bar"
97
+ end
98
+
99
+ it "handles error data on a resource" do
100
+ @user = User.create(:name => "George Michael Bluth")
101
+ @user.response_errors.should == ["Yes", "Sir"]
102
+ end
103
+ end
104
+
105
+ context "mapping data, metadata and error data in string keys to Ruby objects" do
106
+ before do
107
+ api = Her::API.new
108
+ api.setup :url => "https://api.example.com" do |builder|
109
+ builder.use Her::Middleware::SecondLevelParseJSON
110
+ builder.use Faraday::Request::UrlEncoded
111
+ builder.adapter :test do |stub|
112
+ stub.get("/users") { |env| [200, {}, { 'data' => [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }], 'metadata' => { :total_pages => 10, :next_page => 2 }, 'errors' => ["Oh", "My", "God"] }.to_json] }
113
+ stub.post("/users") { |env| [200, {}, { 'data' => { :name => "George Michael Bluth" }, 'metadata' => { :foo => "bar" }, 'errors' => ["Yes", "Sir"] }.to_json] }
114
+ end
115
+ end
116
+
117
+ spawn_model :User do
118
+ uses_api api
119
+ end
120
+ end
121
+
122
+ it "handles metadata on a collection" do
123
+ @users = User.all
124
+ @users.metadata[:total_pages].should == 10
125
+ end
126
+
127
+ it "handles error data on a collection" do
128
+ @users = User.all
129
+ @users.errors.length.should == 3
130
+ end
131
+
132
+ it "handles metadata on a resource" do
133
+ @user = User.create(:name => "George Michael Bluth")
134
+ @user.metadata[:foo].should == "bar"
135
+ end
136
+
137
+ it "handles error data on a resource" do
138
+ @user = User.create(:name => "George Michael Bluth")
139
+ @user.response_errors.should == ["Yes", "Sir"]
140
+ end
141
+ end
142
+
143
+ context "defining custom getters and setters" do
144
+ before do
145
+ api = Her::API.new
146
+ api.setup :url => "https://api.example.com" do |builder|
147
+ builder.use Her::Middleware::FirstLevelParseJSON
148
+ builder.use Faraday::Request::UrlEncoded
149
+ builder.adapter :test do |stub|
150
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :friends => ["Maeby", "GOB", "Anne"] }.to_json] }
151
+ stub.get("/users/2") { |env| [200, {}, { :id => 1 }.to_json] }
152
+ end
153
+ end
154
+
155
+ spawn_model :User do
156
+ uses_api api
157
+ belongs_to :organization
158
+
159
+ def friends=(val)
160
+ val = val.gsub("\r", "").split("\n").map { |friend| friend.gsub(/^\s*\*\s*/, "") } if val and val.is_a?(String)
161
+ @attributes[:friends] = val
162
+ end
163
+
164
+ def friends
165
+ @attributes[:friends].map { |friend| "* #{friend}" }.join("\n")
166
+ end
167
+ end
168
+ end
169
+
170
+ it "handles custom setters" do
171
+ @user = User.find(1)
172
+ @user.friends.should == "* Maeby\n* GOB\n* Anne"
173
+ @user.instance_eval do
174
+ @attributes[:friends] = ["Maeby", "GOB", "Anne"]
175
+ end
176
+ end
177
+
178
+ it "handles custom getters" do
179
+ @user = User.new
180
+ @user.friends = "* George\n* Oscar\n* Lucille"
181
+ @user.friends.should == "* George\n* Oscar\n* Lucille"
182
+ @user.instance_eval do
183
+ @attributes[:friends] = ["George", "Oscar", "Lucille"]
184
+ end
185
+ end
186
+ end
187
+
188
+ context "finding resources" do
189
+ before do
190
+ api = Her::API.new
191
+ api.setup :url => "https://api.example.com" do |builder|
192
+ builder.use Her::Middleware::FirstLevelParseJSON
193
+ builder.use Faraday::Request::UrlEncoded
194
+ builder.adapter :test do |stub|
195
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :age => 42 }.to_json] }
196
+ stub.get("/users/2") { |env| [200, {}, { :id => 2, :age => 34 }.to_json] }
197
+ stub.get("/users?id[]=1&id[]=2") { |env| [200, {}, [{ :id => 1, :age => 42 }, { :id => 2, :age => 34 }].to_json] }
198
+ stub.get("/users?age=42&foo=bar") { |env| [200, {}, [{ :id => 3, :age => 42 }].to_json] }
199
+ stub.get("/users?age=42") { |env| [200, {}, [{ :id => 1, :age => 42 }].to_json] }
200
+ stub.get("/users?age=40") { |env| [200, {}, [{ :id => 1, :age => 40 }].to_json] }
201
+ end
202
+ end
203
+
204
+ spawn_model :User do
205
+ uses_api api
206
+ end
207
+ end
208
+
209
+ it "handles finding by a single id" do
210
+ @user = User.find(1)
211
+ @user.id.should == 1
212
+ end
213
+
214
+ it "handles finding by multiple ids" do
215
+ @users = User.find(1, 2)
216
+ @users.should be_kind_of(Array)
217
+ @users.length.should == 2
218
+ @users[0].id.should == 1
219
+ @users[1].id.should == 2
220
+ end
221
+
222
+ it "handles finding by an array of ids" do
223
+ @users = User.find([1, 2])
224
+ @users.should be_kind_of(Array)
225
+ @users.length.should == 2
226
+ @users[0].id.should == 1
227
+ @users[1].id.should == 2
228
+ end
229
+
230
+ it "handles finding by an array of ids of length 1" do
231
+ @users = User.find([1])
232
+ @users.should be_kind_of(Array)
233
+ @users.length.should == 1
234
+ @users[0].id.should == 1
235
+ end
236
+
237
+ it "handles finding by an array id param of length 2" do
238
+ @users = User.find(id: [1, 2])
239
+ @users.should be_kind_of(Array)
240
+ @users.length.should == 2
241
+ @users[0].id.should == 1
242
+ @users[1].id.should == 2
243
+ end
244
+
245
+ it 'handles finding with id parameter as an array' do
246
+ @users = User.where(id: [1, 2])
247
+ @users.should be_kind_of(Array)
248
+ @users.length.should == 2
249
+ @users[0].id.should == 1
250
+ @users[1].id.should == 2
251
+ end
252
+
253
+ it "handles finding with other parameters" do
254
+ @users = User.where(:age => 42, :foo => "bar").all
255
+ @users.should be_kind_of(Array)
256
+ @users.first.id.should == 3
257
+ end
258
+
259
+ it "handles finding with other parameters and scoped" do
260
+ @users = User.scoped
261
+ @users.where(:age => 42).should be_all { |u| u.age == 42 }
262
+ @users.where(:age => 40).should be_all { |u| u.age == 40 }
263
+ end
264
+ end
265
+
266
+ context "building resources" do
267
+ context "when request_new_object_on_build is not set (default)" do
268
+ before do
269
+ spawn_model("Foo::User")
270
+ end
271
+
272
+ it "builds a new resource without requesting it" do
273
+ Foo::User.should_not_receive(:request)
274
+ @new_user = Foo::User.build(:fullname => "Tobias Fünke")
275
+ @new_user.new?.should be_truthy
276
+ @new_user.fullname.should == "Tobias Fünke"
277
+ end
278
+ end
279
+
280
+ context "when request_new_object_on_build is set" do
281
+ before do
282
+ Her::API.setup :url => "https://api.example.com" do |builder|
283
+ builder.use Her::Middleware::FirstLevelParseJSON
284
+ builder.use Faraday::Request::UrlEncoded
285
+ builder.adapter :test do |stub|
286
+ stub.get("/users/new") { |env| ok! :id => nil, :fullname => params(env)[:fullname], :email => "tobias@bluthcompany.com" }
287
+ end
288
+ end
289
+
290
+ spawn_model("Foo::User") { request_new_object_on_build true }
291
+ end
292
+
293
+ it "requests a new resource" do
294
+ Foo::User.should_receive(:request).once.and_call_original
295
+ @new_user = Foo::User.build(:fullname => "Tobias Fünke")
296
+ @new_user.new?.should be_truthy
297
+ @new_user.fullname.should == "Tobias Fünke"
298
+ @new_user.email.should == "tobias@bluthcompany.com"
299
+ end
300
+ end
301
+ end
302
+
303
+ context "creating resources" do
304
+ before do
305
+ Her::API.setup :url => "https://api.example.com" do |builder|
306
+ builder.use Her::Middleware::FirstLevelParseJSON
307
+ builder.use Faraday::Request::UrlEncoded
308
+ builder.adapter :test do |stub|
309
+ stub.post("/users") { |env| [200, {}, { :id => 1, :fullname => Faraday::Utils.parse_query(env[:body])['fullname'], :email => Faraday::Utils.parse_query(env[:body])['email'] }.to_json] }
310
+ stub.post("/companies") { |env| [200, {}, { :errors => ["name is required"] }.to_json] }
311
+ end
312
+ end
313
+
314
+ spawn_model "Foo::User"
315
+ spawn_model "Foo::Company"
316
+ end
317
+
318
+ it "handle one-line resource creation" do
319
+ @user = Foo::User.create(:fullname => "Tobias Fünke", :email => "tobias@bluth.com")
320
+ @user.id.should == 1
321
+ @user.fullname.should == "Tobias Fünke"
322
+ @user.email.should == "tobias@bluth.com"
323
+ end
324
+
325
+ it "handle resource creation through Model.new + #save" do
326
+ @user = Foo::User.new(:fullname => "Tobias Fünke")
327
+ @user.save.should be_truthy
328
+ @user.fullname.should == "Tobias Fünke"
329
+ end
330
+
331
+ it "handle resource creation through Model.new + #save!" do
332
+ @user = Foo::User.new(:fullname => "Tobias Fünke")
333
+ @user.save!.should be_truthy
334
+ @user.fullname.should == "Tobias Fünke"
335
+ end
336
+
337
+ it "returns false when #save gets errors" do
338
+ @company = Foo::Company.new
339
+ @company.save.should be_falsey
340
+ end
341
+
342
+ it "raises ResourceInvalid when #save! gets errors" do
343
+ @company = Foo::Company.new
344
+ expect { @company.save! }.to raise_error Her::Errors::ResourceInvalid, "Remote validation failed: name is required"
345
+ end
346
+
347
+ it "don't overwrite data if response is empty" do
348
+ @company = Foo::Company.new(:name => 'Company Inc.')
349
+ @company.save.should be_falsey
350
+ @company.name.should == "Company Inc."
351
+ end
352
+ end
353
+
354
+ context "updating resources" do
355
+ before do
356
+ Her::API.setup :url => "https://api.example.com" do |builder|
357
+ builder.use Her::Middleware::FirstLevelParseJSON
358
+ builder.use Faraday::Request::UrlEncoded
359
+ builder.adapter :test do |stub|
360
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Tobias Fünke" }.to_json] }
361
+ stub.put("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Lindsay Fünke" }.to_json] }
362
+ end
363
+ end
364
+
365
+ spawn_model "Foo::User"
366
+ end
367
+
368
+ it "handle resource data update without saving it" do
369
+ @user = Foo::User.find(1)
370
+ @user.fullname.should == "Tobias Fünke"
371
+ @user.fullname = "Kittie Sanchez"
372
+ @user.fullname.should == "Kittie Sanchez"
373
+ end
374
+
375
+ it "handle resource update through the .update class method" do
376
+ @user = Foo::User.save_existing(1, { :fullname => "Lindsay Fünke" })
377
+ @user.fullname.should == "Lindsay Fünke"
378
+ end
379
+
380
+ it "handle resource update through #save on an existing resource" do
381
+ @user = Foo::User.find(1)
382
+ @user.fullname = "Lindsay Fünke"
383
+ @user.save
384
+ @user.fullname.should == "Lindsay Fünke"
385
+ end
386
+ end
387
+
388
+ context "deleting resources" do
389
+ before do
390
+ Her::API.setup :url => "https://api.example.com" do |builder|
391
+ builder.use Her::Middleware::FirstLevelParseJSON
392
+ builder.use Faraday::Request::UrlEncoded
393
+ builder.adapter :test do |stub|
394
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Tobias Fünke", :active => true }.to_json] }
395
+ stub.delete("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Lindsay Fünke", :active => false }.to_json] }
396
+ end
397
+ end
398
+
399
+ spawn_model "Foo::User"
400
+ end
401
+
402
+ it "handle resource deletion through the .destroy class method" do
403
+ @user = Foo::User.destroy_existing(1)
404
+ @user.active.should be_falsey
405
+ @user.should be_destroyed
406
+ end
407
+
408
+ it "handle resource deletion through #destroy on an existing resource" do
409
+ @user = Foo::User.find(1)
410
+ @user.destroy
411
+ @user.active.should be_falsey
412
+ @user.should be_destroyed
413
+ end
414
+ end
415
+
416
+ context 'customizing HTTP methods' do
417
+ before do
418
+ Her::API.setup :url => "https://api.example.com" do |builder|
419
+ builder.use Her::Middleware::FirstLevelParseJSON
420
+ builder.use Faraday::Request::UrlEncoded
421
+ end
422
+ end
423
+
424
+ context 'create' do
425
+ before do
426
+ Her::API.default_api.connection.adapter :test do |stub|
427
+ stub.put('/users') { |env| [200, {}, { :id => 1, :fullname => 'Tobias Fünke' }.to_json] }
428
+ end
429
+ spawn_model 'Foo::User' do
430
+ attributes :fullname, :email
431
+ method_for :create, 'PUT'
432
+ end
433
+ end
434
+
435
+ context 'for top-level class' do
436
+ it 'uses the custom method (PUT) instead of default method (POST)' do
437
+ user = Foo::User.new(:fullname => 'Tobias Fünke')
438
+ user.should be_new
439
+ user.save.should be_truthy
440
+ end
441
+ end
442
+
443
+ context 'for children class' do
444
+ before do
445
+ class User < Foo::User; end
446
+ @spawned_models << :User
447
+ end
448
+
449
+ it 'uses the custom method (PUT) instead of default method (POST)' do
450
+ user = User.new(:fullname => 'Tobias Fünke')
451
+ user.should be_new
452
+ user.save.should be_truthy
453
+ end
454
+ end
455
+ end
456
+
457
+ context 'update' do
458
+ before do
459
+ Her::API.default_api.connection.adapter :test do |stub|
460
+ stub.get('/users/1') { |env| [200, {}, { :id => 1, :fullname => 'Lindsay Fünke' }.to_json] }
461
+ stub.post('/users/1') { |env| [200, {}, { :id => 1, :fullname => 'Tobias Fünke' }.to_json] }
462
+ end
463
+
464
+ spawn_model 'Foo::User' do
465
+ attributes :fullname, :email
466
+ method_for :update, :post
467
+ end
468
+ end
469
+
470
+ it 'uses the custom method (POST) instead of default method (PUT)' do
471
+ user = Foo::User.find(1)
472
+ user.fullname.should eq 'Lindsay Fünke'
473
+ user.fullname = 'Toby Fünke'
474
+ user.save
475
+ user.fullname.should eq 'Tobias Fünke'
476
+ end
477
+ end
478
+ end
479
+ end