her5 0.8.1

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