her5 0.8.1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +17 -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 +1017 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +101 -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/gemfiles/Gemfile.activemodel-5.0.x +7 -0
  17. data/her5.gemspec +30 -0
  18. data/lib/her.rb +19 -0
  19. data/lib/her/api.rb +120 -0
  20. data/lib/her/collection.rb +12 -0
  21. data/lib/her/errors.rb +104 -0
  22. data/lib/her/json_api/model.rb +57 -0
  23. data/lib/her/middleware.rb +12 -0
  24. data/lib/her/middleware/accept_json.rb +17 -0
  25. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  26. data/lib/her/middleware/json_api_parser.rb +68 -0
  27. data/lib/her/middleware/parse_json.rb +28 -0
  28. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  29. data/lib/her/model.rb +75 -0
  30. data/lib/her/model/associations.rb +141 -0
  31. data/lib/her/model/associations/association.rb +107 -0
  32. data/lib/her/model/associations/association_proxy.rb +45 -0
  33. data/lib/her/model/associations/belongs_to_association.rb +101 -0
  34. data/lib/her/model/associations/has_many_association.rb +101 -0
  35. data/lib/her/model/associations/has_one_association.rb +80 -0
  36. data/lib/her/model/attributes.rb +297 -0
  37. data/lib/her/model/base.rb +33 -0
  38. data/lib/her/model/deprecated_methods.rb +61 -0
  39. data/lib/her/model/http.rb +113 -0
  40. data/lib/her/model/introspection.rb +65 -0
  41. data/lib/her/model/nested_attributes.rb +84 -0
  42. data/lib/her/model/orm.rb +207 -0
  43. data/lib/her/model/parse.rb +221 -0
  44. data/lib/her/model/paths.rb +126 -0
  45. data/lib/her/model/relation.rb +164 -0
  46. data/lib/her/version.rb +3 -0
  47. data/spec/api_spec.rb +114 -0
  48. data/spec/collection_spec.rb +26 -0
  49. data/spec/json_api/model_spec.rb +305 -0
  50. data/spec/middleware/accept_json_spec.rb +10 -0
  51. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  52. data/spec/middleware/json_api_parser_spec.rb +32 -0
  53. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  54. data/spec/model/associations/association_proxy_spec.rb +31 -0
  55. data/spec/model/associations_spec.rb +504 -0
  56. data/spec/model/attributes_spec.rb +389 -0
  57. data/spec/model/callbacks_spec.rb +145 -0
  58. data/spec/model/dirty_spec.rb +91 -0
  59. data/spec/model/http_spec.rb +158 -0
  60. data/spec/model/introspection_spec.rb +76 -0
  61. data/spec/model/nested_attributes_spec.rb +134 -0
  62. data/spec/model/orm_spec.rb +506 -0
  63. data/spec/model/parse_spec.rb +345 -0
  64. data/spec/model/paths_spec.rb +347 -0
  65. data/spec/model/relation_spec.rb +226 -0
  66. data/spec/model/validations_spec.rb +42 -0
  67. data/spec/model_spec.rb +44 -0
  68. data/spec/spec_helper.rb +26 -0
  69. data/spec/support/extensions/array.rb +5 -0
  70. data/spec/support/extensions/hash.rb +5 -0
  71. data/spec/support/macros/her_macros.rb +17 -0
  72. data/spec/support/macros/model_macros.rb +36 -0
  73. data/spec/support/macros/request_macros.rb +27 -0
  74. metadata +289 -0
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Her::Middleware::AcceptJSON do
5
+ it "adds an Accept header" do
6
+ described_class.new.add_header({}).tap do |headers|
7
+ headers["Accept"].should == "application/json"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Her::Middleware::FirstLevelParseJSON do
5
+ subject { described_class.new }
6
+ let(:body_without_errors) { "{\"id\": 1, \"name\": \"Tobias Fünke\", \"metadata\": 3}" }
7
+ let(:body_with_errors) { "{\"id\": 1, \"name\": \"Tobias Fünke\", \"errors\": { \"name\": [ \"not_valid\", \"should_be_present\" ] }, \"metadata\": 3}" }
8
+ let(:body_with_malformed_json) { "wut." }
9
+ let(:body_with_invalid_json) { "true" }
10
+ let(:empty_body) { '' }
11
+ let(:nil_body) { nil }
12
+
13
+ it "parses body as json" do
14
+ subject.parse(body_without_errors).tap do |json|
15
+ json[:data].should == { :id => 1, :name => "Tobias Fünke" }
16
+ json[:metadata].should == 3
17
+ end
18
+ end
19
+
20
+ it "parses :body key as json in the env hash" do
21
+ env = { :body => body_without_errors }
22
+ subject.on_complete(env)
23
+ env[:body].tap do |json|
24
+ json[:data].should == { :id => 1, :name => "Tobias Fünke" }
25
+ json[:metadata].should == 3
26
+ end
27
+ end
28
+
29
+ it 'ensures the errors are a hash if there are no errors' do
30
+ subject.parse(body_without_errors)[:errors].should eq({})
31
+ end
32
+
33
+ it 'ensures the errors are a hash if there are no errors' do
34
+ subject.parse(body_with_errors)[:errors].should eq({:name => [ 'not_valid', 'should_be_present']})
35
+ end
36
+
37
+ it 'ensures that malformed JSON throws an exception' do
38
+ expect { subject.parse(body_with_malformed_json) }.to raise_error(Her::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "wut.")')
39
+ end
40
+
41
+ it 'ensures that invalid JSON throws an exception' do
42
+ expect { subject.parse(body_with_invalid_json) }.to raise_error(Her::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "true")')
43
+ end
44
+
45
+ it 'ensures that a nil response returns an empty hash' do
46
+ subject.parse(nil_body)[:data].should eq({})
47
+ end
48
+
49
+ it 'ensures that an empty response returns an empty hash' do
50
+ subject.parse(empty_body)[:data].should eq({})
51
+ end
52
+
53
+ context 'with status code 204' do
54
+ it 'returns an empty body' do
55
+ env = { :status => 204 }
56
+ subject.on_complete(env)
57
+ env[:body].tap do |json|
58
+ json[:data].should == { }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Her::Middleware::JsonApiParser do
5
+ subject { described_class.new }
6
+
7
+ context "with valid JSON body" do
8
+ let(:body) { '{"data": {"type": "foo", "id": "bar", "attributes": {"baz": "qux"} }, "meta": {"api": "json api"} }' }
9
+ let(:env) { { body: body } }
10
+
11
+ it "parses body as json" do
12
+ subject.on_complete(env)
13
+ env.fetch(:body).tap do |json|
14
+ expect(json[:data]).to eql(
15
+ :type => "foo",
16
+ :id => "bar",
17
+ :attributes => { :baz => "qux" }
18
+ )
19
+ expect(json[:errors]).to eql([])
20
+ expect(json[:metadata]).to eql(:api => "json api")
21
+ end
22
+ end
23
+ end
24
+
25
+ #context "with invalid JSON body" do
26
+ # let(:body) { '"foo"' }
27
+ # it 'ensures that invalid JSON throws an exception' do
28
+ # expect { subject.parse(body) }.to raise_error(Her::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "\"foo\"")')
29
+ # end
30
+ #end
31
+
32
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Her::Middleware::SecondLevelParseJSON do
5
+ subject { described_class.new }
6
+
7
+ context "with valid JSON body" do
8
+ let(:body) { "{\"data\": 1, \"errors\": 2, \"metadata\": 3}" }
9
+ it "parses body as json" do
10
+ subject.parse(body).tap do |json|
11
+ json[:data].should == 1
12
+ json[:errors].should == 2
13
+ json[:metadata].should == 3
14
+ end
15
+ end
16
+
17
+ it "parses :body key as json in the env hash" do
18
+ env = { :body => body }
19
+ subject.on_complete(env)
20
+ env[:body].tap do |json|
21
+ json[:data].should == 1
22
+ json[:errors].should == 2
23
+ json[:metadata].should == 3
24
+ end
25
+ end
26
+ end
27
+
28
+ context "with invalid JSON body" do
29
+ let(:body) { '"foo"' }
30
+ it 'ensures that invalid JSON throws an exception' do
31
+ expect { subject.parse(body) }.to raise_error(Her::Errors::ParseError, 'Response from the API must behave like a Hash or an Array (last JSON response was "\"foo\"")')
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ describe Her::Model::Associations::AssociationProxy do
5
+ describe "proxy assignment methods" 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.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke" }.to_json ] }
12
+ stub.get("/users/1/fish") { |env| [200, {}, { :id => 1, :name => "Tobias's Fish" }.to_json ] }
13
+ end
14
+ end
15
+ spawn_model "User" do
16
+ has_one :fish
17
+ end
18
+ spawn_model "Fish"
19
+ end
20
+
21
+ subject { User.find(1) }
22
+
23
+ it "should assign value" do
24
+ subject.fish.name = "Fishy"
25
+ expect(subject.fish.name).to eq "Fishy"
26
+ end
27
+ end
28
+ end
29
+
30
+
31
+
@@ -0,0 +1,504 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
3
+
4
+ describe Her::Model::Associations do
5
+ context "setting associations without details" do
6
+ before { spawn_model "Foo::User" }
7
+ subject { Foo::User.associations }
8
+
9
+ context "single has_many association" do
10
+ before { Foo::User.has_many :comments }
11
+ its([:has_many]) { should eql [{ :name => :comments, :data_key => :comments, :default => [], :class_name => "Comment", :path => "/comments", :inverse_of => nil }] }
12
+ end
13
+
14
+ context "multiple has_many associations" do
15
+ before do
16
+ Foo::User.has_many :comments
17
+ Foo::User.has_many :posts
18
+ end
19
+
20
+ its([:has_many]) { should eql [{ :name => :comments, :data_key => :comments, :default => [], :class_name => "Comment", :path => "/comments", :inverse_of => nil }, { :name => :posts, :data_key => :posts, :default => [], :class_name => "Post", :path => "/posts", :inverse_of => nil }] }
21
+ end
22
+
23
+ context "single has_one association" do
24
+ before { Foo::User.has_one :category }
25
+ its([:has_one]) { should eql [{ :name => :category, :data_key => :category, :default => nil, :class_name => "Category", :path => "/category" }] }
26
+ end
27
+
28
+ context "multiple has_one associations" do
29
+ before do
30
+ Foo::User.has_one :category
31
+ Foo::User.has_one :role
32
+ end
33
+
34
+ its([:has_one]) { should eql [{ :name => :category, :data_key => :category, :default => nil, :class_name => "Category", :path => "/category" }, { :name => :role, :data_key => :role, :default => nil, :class_name => "Role", :path => "/role" }] }
35
+ end
36
+
37
+ context "single belongs_to association" do
38
+ before { Foo::User.belongs_to :organization }
39
+ its([:belongs_to]) { should eql [{ :name => :organization, :data_key => :organization, :default => nil, :class_name => "Organization", :foreign_key => "organization_id", :path => "/organizations/:id" }] }
40
+ end
41
+
42
+ context "multiple belongs_to association" do
43
+ before do
44
+ Foo::User.belongs_to :organization
45
+ Foo::User.belongs_to :family
46
+ end
47
+
48
+ its([:belongs_to]) { should eql [{ :name => :organization, :data_key => :organization, :default => nil, :class_name => "Organization", :foreign_key => "organization_id", :path => "/organizations/:id" }, { :name => :family, :data_key => :family, :default => nil, :class_name => "Family", :foreign_key => "family_id", :path => "/families/:id" }] }
49
+ end
50
+ end
51
+
52
+ context "setting associations with details" do
53
+ before { spawn_model "Foo::User" }
54
+ subject { Foo::User.associations }
55
+
56
+ context "in base class" do
57
+ context "single has_many association" do
58
+ before { Foo::User.has_many :comments, :class_name => "Post", :inverse_of => :admin, :data_key => :user_comments, :default => {} }
59
+ its([:has_many]) { should eql [{ :name => :comments, :data_key => :user_comments, :default => {}, :class_name => "Post", :path => "/comments", :inverse_of => :admin }] }
60
+ end
61
+
62
+ context "single has_one association" do
63
+ before { Foo::User.has_one :category, :class_name => "Topic", :foreign_key => "topic_id", :data_key => :topic, :default => nil }
64
+ its([:has_one]) { should eql [{ :name => :category, :data_key => :topic, :default => nil, :class_name => "Topic", :foreign_key => "topic_id", :path => "/category" }] }
65
+ end
66
+
67
+ context "single belongs_to association" do
68
+ before { Foo::User.belongs_to :organization, :class_name => "Business", :foreign_key => "org_id", :data_key => :org, :default => true }
69
+ its([:belongs_to]) { should eql [{ :name => :organization, :data_key => :org, :default => true, :class_name => "Business", :foreign_key => "org_id", :path => "/organizations/:id" }] }
70
+ end
71
+ end
72
+
73
+ context "in parent class" do
74
+ before { Foo::User.has_many :comments, :class_name => "Post" }
75
+
76
+ describe "associations accessor" do
77
+ subject { Class.new(Foo::User).associations }
78
+ its(:object_id) { should_not eql Foo::User.associations.object_id }
79
+ its([:has_many]) { should eql [{ :name => :comments, :data_key => :comments, :default => [], :class_name => "Post", :path => "/comments", :inverse_of => nil }] }
80
+ end
81
+ end
82
+ end
83
+
84
+ context "handling associations without details" do
85
+ before do
86
+ Her::API.setup :url => "https://api.example.com" do |builder|
87
+ builder.use Her::Middleware::FirstLevelParseJSON
88
+ builder.use Faraday::Request::UrlEncoded
89
+ builder.adapter :test do |stub|
90
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :comments => [{ :comment => { :id => 2, :body => "Tobias, you blow hard!", :user_id => 1 } }, { :comment => { :id => 3, :body => "I wouldn't mind kissing that man between the cheeks, so to speak", :user_id => 1 } }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 1, :name => "Bluth Company" }, :organization_id => 1 }.to_json] }
91
+ stub.get("/users/2") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :organization_id => 2 }.to_json] }
92
+ stub.get("/users/1/comments") { |env| [200, {}, [{ :comment => { :id => 4, :body => "They're having a FIRESALE?" } }].to_json] }
93
+ stub.get("/users/2/comments") { |env| [200, {}, [{ :comment => { :id => 4, :body => "They're having a FIRESALE?" } }, { :comment => { :id => 5, :body => "Is this the tiny town from Footloose?" } }].to_json] }
94
+ stub.get("/users/2/comments/5") { |env| [200, {}, { :comment => { :id => 5, :body => "Is this the tiny town from Footloose?" } }.to_json] }
95
+ stub.get("/users/2/role") { |env| [200, {}, { :id => 2, :body => "User" }.to_json] }
96
+ stub.get("/users/1/role") { |env| [200, {}, { :id => 3, :body => "User" }.to_json] }
97
+ stub.get("/users/1/posts") { |env| [200, {}, [{:id => 1, :body => 'blogging stuff', :admin_id => 1 }].to_json] }
98
+ stub.get("/organizations/1") { |env| [200, {}, { :organization => { :id => 1, :name => "Bluth Company Foo" } }.to_json] }
99
+ stub.post("/users") { |env| [200, {}, { :id => 5, :name => "Mr. Krabs", :comments => [{ :comment => { :id => 99, :body => "Rodríguez, nasibisibusi?", :user_id => 5 } }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 3, :name => "Krusty Krab" }, :organization_id => 3 }.to_json] }
100
+ stub.put("/users/5") { |env| [200, {}, { :id => 5, :name => "Clancy Brown", :comments => [{ :comment => { :id => 99, :body => "Rodríguez, nasibisibusi?", :user_id => 5 } }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 3, :name => "Krusty Krab" }, :organization_id => 3 }.to_json] }
101
+ stub.delete("/users/5") { |env| [200, {}, { :id => 5, :name => "Clancy Brown", :comments => [{ :comment => { :id => 99, :body => "Rodríguez, nasibisibusi?", :user_id => 5 } }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 3, :name => "Krusty Krab" }, :organization_id => 3 }.to_json] }
102
+
103
+ stub.get("/organizations/2") do |env|
104
+ if env[:params]["admin"] == "true"
105
+ [200, {}, { :organization => { :id => 2, :name => "Bluth Company (admin)" } }.to_json]
106
+ else
107
+ [200, {}, { :organization => { :id => 2, :name => "Bluth Company" } }.to_json]
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ spawn_model "Foo::User" do
114
+ has_many :comments, class_name: "Foo::Comment"
115
+ has_one :role
116
+ belongs_to :organization
117
+ has_many :posts, :inverse_of => :admin
118
+ end
119
+ spawn_model "Foo::Comment" do
120
+ belongs_to :user
121
+ parse_root_in_json true
122
+ end
123
+ spawn_model "Foo::Post" do
124
+ belongs_to :admin, :class_name => 'Foo::User'
125
+ end
126
+
127
+ spawn_model "Foo::Organization" do
128
+ parse_root_in_json true
129
+ end
130
+
131
+ spawn_model "Foo::Role"
132
+
133
+ @user_with_included_data = Foo::User.find(1)
134
+ @user_without_included_data = Foo::User.find(2)
135
+ @user_without_organization_and_not_persisted = Foo::User.new(organization_id: nil, name: "Katlin Fünke")
136
+ end
137
+
138
+ let(:user_with_included_data_after_create) { Foo::User.create }
139
+ let(:user_with_included_data_after_save_existing) { Foo::User.save_existing(5, :name => "Clancy Brown") }
140
+ let(:user_with_included_data_after_destroy) { Foo::User.new(:id => 5).destroy }
141
+ let(:comment_without_included_parent_data) { Foo::Comment.new(:id => 7, :user_id => 1) }
142
+
143
+ it "maps an array of included data through has_many" do
144
+ @user_with_included_data.comments.first.should be_a(Foo::Comment)
145
+ @user_with_included_data.comments.length.should == 2
146
+ @user_with_included_data.comments.first.id.should == 2
147
+ @user_with_included_data.comments.first.body.should == "Tobias, you blow hard!"
148
+ end
149
+
150
+ it "does not refetch the parents models data if they have been fetched before" do
151
+ @user_with_included_data.comments.first.user.object_id.should == @user_with_included_data.object_id
152
+ end
153
+
154
+ it "does fetch the parent models data only once" do
155
+ comment_without_included_parent_data.user.object_id.should == comment_without_included_parent_data.user.object_id
156
+ end
157
+
158
+ it "does fetch the parent models data that was cached if called with parameters" do
159
+ comment_without_included_parent_data.user.object_id.should_not == comment_without_included_parent_data.user.where(:a => 2).object_id
160
+ end
161
+
162
+ it "uses the given inverse_of key to set the parent model" do
163
+ @user_with_included_data.posts.first.admin.object_id.should == @user_with_included_data.object_id
164
+ end
165
+
166
+ it "fetches data that was not included through has_many" do
167
+ @user_without_included_data.comments.first.should be_a(Foo::Comment)
168
+ @user_without_included_data.comments.length.should == 2
169
+ @user_without_included_data.comments.first.id.should == 4
170
+ @user_without_included_data.comments.first.body.should == "They're having a FIRESALE?"
171
+ end
172
+
173
+ it "fetches has_many data even if it was included, only if called with parameters" do
174
+ @user_with_included_data.comments.where(:foo_id => 1).length.should == 1
175
+ end
176
+
177
+ it "fetches data that was not included through has_many only once" do
178
+ @user_without_included_data.comments.first.object_id.should == @user_without_included_data.comments.first.object_id
179
+ end
180
+
181
+ it "fetches data that was cached through has_many if called with parameters" do
182
+ @user_without_included_data.comments.first.object_id.should_not == @user_without_included_data.comments.where(:foo_id => 1).first.object_id
183
+ end
184
+
185
+ it "maps an array of included data through has_one" do
186
+ @user_with_included_data.role.should be_a(Foo::Role)
187
+ @user_with_included_data.role.object_id.should == @user_with_included_data.role.object_id
188
+ @user_with_included_data.role.id.should == 1
189
+ @user_with_included_data.role.body.should == "Admin"
190
+ end
191
+
192
+ it "fetches data that was not included through has_one" do
193
+ @user_without_included_data.role.should be_a(Foo::Role)
194
+ @user_without_included_data.role.id.should == 2
195
+ @user_without_included_data.role.body.should == "User"
196
+ end
197
+
198
+ it "fetches has_one data even if it was included, only if called with parameters" do
199
+ @user_with_included_data.role.where(:foo_id => 2).id.should == 3
200
+ end
201
+
202
+ it "maps an array of included data through belongs_to" do
203
+ @user_with_included_data.organization.should be_a(Foo::Organization)
204
+ @user_with_included_data.organization.id.should == 1
205
+ @user_with_included_data.organization.name.should == "Bluth Company"
206
+ end
207
+
208
+ it "fetches data that was not included through belongs_to" do
209
+ @user_without_included_data.organization.should be_a(Foo::Organization)
210
+ @user_without_included_data.organization.id.should == 2
211
+ @user_without_included_data.organization.name.should == "Bluth Company"
212
+ end
213
+
214
+ it "returns nil if the foreign key is nil" do
215
+ @user_without_organization_and_not_persisted.organization.should be_nil
216
+ end
217
+
218
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
219
+ @user_with_included_data.organization.where(:foo_id => 1).name.should == "Bluth Company Foo"
220
+ end
221
+
222
+ it "can tell if it has a association" do
223
+ @user_without_included_data.has_association?(:unknown_association).should be false
224
+ @user_without_included_data.has_association?(:organization).should be true
225
+ end
226
+
227
+ it "fetches the resource corresponding to a named association" do
228
+ @user_without_included_data.get_association(:unknown_association).should be_nil
229
+ @user_without_included_data.get_association(:organization).name.should == "Bluth Company"
230
+ end
231
+
232
+ it "pass query string parameters when additional arguments are passed" do
233
+ @user_without_included_data.organization.where(:admin => true).name.should == "Bluth Company (admin)"
234
+ @user_without_included_data.organization.name.should == "Bluth Company"
235
+ end
236
+
237
+ it "fetches data with the specified id when calling find" do
238
+ comment = @user_without_included_data.comments.find(5)
239
+ comment.should be_a(Foo::Comment)
240
+ comment.id.should eq(5)
241
+ end
242
+
243
+ it "'s associations responds to #empty?" do
244
+ @user_without_included_data.organization.respond_to?(:empty?).should be_truthy
245
+ @user_without_included_data.organization.should_not be_empty
246
+ end
247
+
248
+ it 'includes has_many relationships in params by default' do
249
+ params = @user_with_included_data.to_params
250
+ params[:comments].should be_kind_of(Array)
251
+ params[:comments].length.should eq(2)
252
+ end
253
+
254
+ [:create, :save_existing, :destroy].each do |type|
255
+ context "after #{type}" do
256
+ let(:subject) { self.send("user_with_included_data_after_#{type}")}
257
+
258
+ it "maps an array of included data through has_many" do
259
+ subject.comments.first.should be_a(Foo::Comment)
260
+ subject.comments.length.should == 1
261
+ subject.comments.first.id.should == 99
262
+ subject.comments.first.body.should == "Rodríguez, nasibisibusi?"
263
+ end
264
+
265
+ it "maps an array of included data through has_one" do
266
+ subject.role.should be_a(Foo::Role)
267
+ subject.role.id.should == 1
268
+ subject.role.body.should == "Admin"
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ context "handling associations with details in active_model_serializers format" do
275
+ before do
276
+ Her::API.setup :url => "https://api.example.com" do |builder|
277
+ builder.use Her::Middleware::FirstLevelParseJSON
278
+ builder.use Faraday::Request::UrlEncoded
279
+ builder.adapter :test do |stub|
280
+ stub.get("/users/1") { |env| [200, {}, { :user => { :id => 1, :name => "Tobias Fünke", :comments => [{ :id => 2, :body => "Tobias, you blow hard!", :user_id => 1 }, { :id => 3, :body => "I wouldn't mind kissing that man between the cheeks, so to speak", :user_id => 1 }], :role => { :id => 1, :body => "Admin" }, :organization => { :id => 1, :name => "Bluth Company" }, :organization_id => 1 } }.to_json] }
281
+ stub.get("/users/2") { |env| [200, {}, { :user => { :id => 2, :name => "Lindsay Fünke", :organization_id => 1 } }.to_json] }
282
+ stub.get("/users/1/comments") { |env| [200, {}, { :comments => [{ :id => 4, :body => "They're having a FIRESALE?" }] }.to_json] }
283
+ stub.get("/users/2/comments") { |env| [200, {}, { :comments => [{ :id => 4, :body => "They're having a FIRESALE?" }, { :id => 5, :body => "Is this the tiny town from Footloose?" }] }.to_json] }
284
+ stub.get("/users/2/comments/5") { |env| [200, {}, { :comment => { :id => 5, :body => "Is this the tiny town from Footloose?" } }.to_json] }
285
+ stub.get("/organizations/1") { |env| [200, {}, { :organization => { :id => 1, :name => "Bluth Company Foo" } }.to_json] }
286
+ end
287
+ end
288
+ spawn_model "Foo::User" do
289
+ parse_root_in_json true, :format => :active_model_serializers
290
+ has_many :comments, class_name: "Foo::Comment"
291
+ belongs_to :organization
292
+ end
293
+ spawn_model "Foo::Comment" do
294
+ belongs_to :user
295
+ parse_root_in_json true, :format => :active_model_serializers
296
+ end
297
+ spawn_model "Foo::Organization" do
298
+ parse_root_in_json true, :format => :active_model_serializers
299
+ end
300
+
301
+ @user_with_included_data = Foo::User.find(1)
302
+ @user_without_included_data = Foo::User.find(2)
303
+ end
304
+
305
+ it "maps an array of included data through has_many" do
306
+ @user_with_included_data.comments.first.should be_a(Foo::Comment)
307
+ @user_with_included_data.comments.length.should == 2
308
+ @user_with_included_data.comments.first.id.should == 2
309
+ @user_with_included_data.comments.first.body.should == "Tobias, you blow hard!"
310
+ end
311
+
312
+ it "does not refetch the parents models data if they have been fetched before" do
313
+ @user_with_included_data.comments.first.user.object_id.should == @user_with_included_data.object_id
314
+ end
315
+
316
+ it "fetches data that was not included through has_many" do
317
+ @user_without_included_data.comments.first.should be_a(Foo::Comment)
318
+ @user_without_included_data.comments.length.should == 2
319
+ @user_without_included_data.comments.first.id.should == 4
320
+ @user_without_included_data.comments.first.body.should == "They're having a FIRESALE?"
321
+ end
322
+
323
+ it "fetches has_many data even if it was included, only if called with parameters" do
324
+ @user_with_included_data.comments.where(:foo_id => 1).length.should == 1
325
+ end
326
+
327
+ it "maps an array of included data through belongs_to" do
328
+ @user_with_included_data.organization.should be_a(Foo::Organization)
329
+ @user_with_included_data.organization.id.should == 1
330
+ @user_with_included_data.organization.name.should == "Bluth Company"
331
+ end
332
+
333
+ it "fetches data that was not included through belongs_to" do
334
+ @user_without_included_data.organization.should be_a(Foo::Organization)
335
+ @user_without_included_data.organization.id.should == 1
336
+ @user_without_included_data.organization.name.should == "Bluth Company Foo"
337
+ end
338
+
339
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
340
+ @user_with_included_data.organization.where(:foo_id => 1).name.should == "Bluth Company Foo"
341
+ end
342
+
343
+ it "fetches data with the specified id when calling find" do
344
+ comment = @user_without_included_data.comments.find(5)
345
+ comment.should be_a(Foo::Comment)
346
+ comment.id.should eq(5)
347
+ end
348
+
349
+ it 'includes has_many relationships in params by default' do
350
+ params = @user_with_included_data.to_params
351
+ params[:comments].should be_kind_of(Array)
352
+ params[:comments].length.should eq(2)
353
+ end
354
+ end
355
+
356
+ context "handling associations with details" do
357
+ before do
358
+ Her::API.setup :url => "https://api.example.com" do |builder|
359
+ builder.use Her::Middleware::FirstLevelParseJSON
360
+ builder.use Faraday::Request::UrlEncoded
361
+ builder.adapter :test do |stub|
362
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :organization => { :id => 1, :name => "Bluth Company Inc." }, :organization_id => 1 }.to_json] }
363
+ stub.get("/users/4") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :organization => { :id => 1, :name => "Bluth Company Inc." } }.to_json] }
364
+ stub.get("/users/2") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :organization_id => 1 }.to_json] }
365
+ stub.get("/users/3") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :company => nil }.to_json] }
366
+ stub.get("/companies/1") { |env| [200, {}, { :id => 1, :name => "Bluth Company" }.to_json] }
367
+ end
368
+ end
369
+
370
+ spawn_model "Foo::User" do
371
+ belongs_to :company, :path => "/organizations/:id", :foreign_key => :organization_id, :data_key => :organization
372
+ end
373
+
374
+ spawn_model "Foo::Company"
375
+
376
+ @user_with_included_data = Foo::User.find(1)
377
+ @user_without_included_data = Foo::User.find(2)
378
+ @user_with_included_nil_data = Foo::User.find(3)
379
+ @user_with_included_data_but_no_fk = Foo::User.find(4)
380
+ end
381
+
382
+ it "maps an array of included data through belongs_to" do
383
+ @user_with_included_data.company.should be_a(Foo::Company)
384
+ @user_with_included_data.company.id.should == 1
385
+ @user_with_included_data.company.name.should == "Bluth Company Inc."
386
+ end
387
+
388
+ it "does not map included data if it’s nil" do
389
+ @user_with_included_nil_data.company.should be_nil
390
+ end
391
+
392
+ it "fetches data that was not included through belongs_to" do
393
+ @user_without_included_data.company.should be_a(Foo::Company)
394
+ @user_without_included_data.company.id.should == 1
395
+ @user_without_included_data.company.name.should == "Bluth Company"
396
+ end
397
+
398
+ it "does not require foreugn key to have nested object" do
399
+ @user_with_included_data_but_no_fk.company.name.should == "Bluth Company Inc."
400
+ end
401
+ end
402
+
403
+ context "object returned by the association method" do
404
+ before do
405
+ spawn_model "Foo::Role" do
406
+ def present?
407
+ "of_course"
408
+ end
409
+ end
410
+ spawn_model "Foo::User" do
411
+ has_one :role
412
+ end
413
+ end
414
+
415
+ let(:associated_value) { Foo::Role.new }
416
+ let(:user_with_role) do
417
+ Foo::User.new.tap { |user| user.role = associated_value }
418
+ end
419
+
420
+ subject { user_with_role.role }
421
+
422
+ it "doesnt mask the object's basic methods" do
423
+ subject.class.should == Foo::Role
424
+ end
425
+
426
+ it "doesnt mask core methods like extend" do
427
+ committer = Module.new
428
+ subject.extend committer
429
+ associated_value.should be_kind_of committer
430
+ end
431
+
432
+ it "can return the association object" do
433
+ subject.association.should be_kind_of Her::Model::Associations::Association
434
+ end
435
+
436
+ it "still can call fetch via the association" do
437
+ subject.association.fetch.should eq associated_value
438
+ end
439
+
440
+ it "calls missing methods on associated value" do
441
+ subject.present?.should == "of_course"
442
+ end
443
+
444
+ it "can use association methods like where" do
445
+ subject.where(role: 'committer').association.
446
+ params.should include :role
447
+ end
448
+ end
449
+
450
+ context "building and creating association data" do
451
+ before do
452
+ spawn_model "Foo::Comment"
453
+ spawn_model "Foo::User" do
454
+ has_many :comments
455
+ end
456
+ end
457
+
458
+ context "with #build" do
459
+ it "takes the parent primary key" do
460
+ @comment = Foo::User.new(:id => 10).comments.build(:body => "Hello!")
461
+ @comment.body.should == "Hello!"
462
+ @comment.user_id.should == 10
463
+ end
464
+ end
465
+
466
+ context "with #create" do
467
+ before do
468
+ Her::API.setup :url => "https://api.example.com" do |builder|
469
+ builder.use Her::Middleware::FirstLevelParseJSON
470
+ builder.use Faraday::Request::UrlEncoded
471
+ builder.adapter :test do |stub|
472
+ stub.get("/users/10") { |env| [200, {}, { :id => 10 }.to_json] }
473
+ stub.post("/comments") { |env| [200, {}, { :id => 1, :body => Faraday::Utils.parse_query(env[:body])['body'], :user_id => Faraday::Utils.parse_query(env[:body])['user_id'].to_i }.to_json] }
474
+ end
475
+ end
476
+
477
+ Foo::User.use_api Her::API.default_api
478
+ Foo::Comment.use_api Her::API.default_api
479
+ end
480
+
481
+ it "takes the parent primary key and saves the resource" do
482
+ @user = Foo::User.find(10)
483
+ @comment = @user.comments.create(:body => "Hello!")
484
+ @comment.id.should == 1
485
+ @comment.body.should == "Hello!"
486
+ @comment.user_id.should == 10
487
+ @user.comments.should == [@comment]
488
+ end
489
+ end
490
+
491
+ context "with #new" do
492
+ it "creates nested models from hash attibutes" do
493
+ user = Foo::User.new(:name => "vic", :comments => [{:text => "hello"}])
494
+ user.comments.first.text.should == "hello"
495
+ end
496
+
497
+ it "assigns nested models if given as already constructed objects" do
498
+ bye = Foo::Comment.new(:text => "goodbye")
499
+ user = Foo::User.new(:name => 'vic', :comments => [bye])
500
+ user.comments.first.text.should == 'goodbye'
501
+ end
502
+ end
503
+ end
504
+ end