herr 0.7.3

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