herr 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +15 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +990 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +81 -0
  12. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  13. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  16. data/her.gemspec +30 -0
  17. data/lib/her.rb +16 -0
  18. data/lib/her/api.rb +115 -0
  19. data/lib/her/collection.rb +12 -0
  20. data/lib/her/errors.rb +27 -0
  21. data/lib/her/middleware.rb +10 -0
  22. data/lib/her/middleware/accept_json.rb +17 -0
  23. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  24. data/lib/her/middleware/parse_json.rb +21 -0
  25. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  26. data/lib/her/model.rb +72 -0
  27. data/lib/her/model/associations.rb +141 -0
  28. data/lib/her/model/associations/association.rb +103 -0
  29. data/lib/her/model/associations/association_proxy.rb +46 -0
  30. data/lib/her/model/associations/belongs_to_association.rb +96 -0
  31. data/lib/her/model/associations/has_many_association.rb +100 -0
  32. data/lib/her/model/associations/has_one_association.rb +79 -0
  33. data/lib/her/model/attributes.rb +266 -0
  34. data/lib/her/model/base.rb +33 -0
  35. data/lib/her/model/deprecated_methods.rb +61 -0
  36. data/lib/her/model/http.rb +114 -0
  37. data/lib/her/model/introspection.rb +65 -0
  38. data/lib/her/model/nested_attributes.rb +45 -0
  39. data/lib/her/model/orm.rb +205 -0
  40. data/lib/her/model/parse.rb +227 -0
  41. data/lib/her/model/paths.rb +121 -0
  42. data/lib/her/model/relation.rb +164 -0
  43. data/lib/her/version.rb +3 -0
  44. data/spec/api_spec.rb +131 -0
  45. data/spec/collection_spec.rb +26 -0
  46. data/spec/middleware/accept_json_spec.rb +10 -0
  47. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  48. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  49. data/spec/model/associations_spec.rb +416 -0
  50. data/spec/model/attributes_spec.rb +268 -0
  51. data/spec/model/callbacks_spec.rb +145 -0
  52. data/spec/model/dirty_spec.rb +86 -0
  53. data/spec/model/http_spec.rb +194 -0
  54. data/spec/model/introspection_spec.rb +76 -0
  55. data/spec/model/nested_attributes_spec.rb +134 -0
  56. data/spec/model/orm_spec.rb +479 -0
  57. data/spec/model/parse_spec.rb +373 -0
  58. data/spec/model/paths_spec.rb +341 -0
  59. data/spec/model/relation_spec.rb +226 -0
  60. data/spec/model/validations_spec.rb +42 -0
  61. data/spec/model_spec.rb +31 -0
  62. data/spec/spec_helper.rb +26 -0
  63. data/spec/support/extensions/array.rb +5 -0
  64. data/spec/support/extensions/hash.rb +5 -0
  65. data/spec/support/macros/her_macros.rb +17 -0
  66. data/spec/support/macros/model_macros.rb +29 -0
  67. data/spec/support/macros/request_macros.rb +27 -0
  68. metadata +280 -0
@@ -0,0 +1,3 @@
1
+ module Her
2
+ VERSION = "0.7.3"
3
+ end
@@ -0,0 +1,131 @@
1
+ # encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "spec_helper.rb")
3
+
4
+ describe Her::API do
5
+ subject { Her::API.new }
6
+
7
+ context "initialization" do
8
+ describe ".setup" do
9
+ it "creates a default connection" do
10
+ Her::API.setup :url => "https://api.example.com"
11
+ Her::API.default_api.base_uri.should == "https://api.example.com"
12
+ end
13
+ end
14
+
15
+ describe "#setup" do
16
+ context "when using :url option" do
17
+ before { subject.setup :url => "https://api.example.com" }
18
+ its(:base_uri) { should == "https://api.example.com" }
19
+ end
20
+
21
+ context "when using the legacy :base_uri option" do
22
+ before { subject.setup :base_uri => "https://api.example.com" }
23
+ its(:base_uri) { should == "https://api.example.com" }
24
+ end
25
+
26
+ context "when setting custom middleware" do
27
+ before do
28
+ class Foo; end;
29
+ class Bar; end;
30
+
31
+ subject.setup :url => "https://api.example.com" do |connection|
32
+ connection.use Foo
33
+ connection.use Bar
34
+ end
35
+ end
36
+
37
+ specify { subject.connection.builder.handlers.should == [Foo, Bar] }
38
+ end
39
+
40
+ context "when setting custom options" do
41
+ before { subject.setup :foo => { :bar => "baz" }, :url => "https://api.example.com" }
42
+ its(:options) { should == { :foo => { :bar => "baz" }, :url => "https://api.example.com" } }
43
+ end
44
+ end
45
+
46
+ describe "#request" do
47
+ before do
48
+ class SimpleParser < Faraday::Response::Middleware
49
+ def on_complete(env)
50
+ env[:body] = { :data => env[:body] }
51
+ end
52
+ end
53
+ end
54
+
55
+ context "making HTTP requests" do
56
+ let(:parsed_data) { subject.request(:_method => :get, :_path => "/foo")[:parsed_data] }
57
+ before do
58
+ subject.setup :url => "https://api.example.com" do |builder|
59
+ builder.use SimpleParser
60
+ builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is."] } }
61
+ end
62
+ end
63
+
64
+ specify { parsed_data[:data].should == "Foo, it is." }
65
+ end
66
+
67
+ context "making HTTP requests while specifying custom HTTP headers" do
68
+ let(:parsed_data) { subject.request(:_method => :get, :_path => "/foo", :_headers => { "X-Page" => 2 })[:parsed_data] }
69
+
70
+ before do
71
+ subject.setup :url => "https://api.example.com" do |builder|
72
+ builder.use SimpleParser
73
+ builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is page #{env[:request_headers]["X-Page"]}."] } }
74
+ end
75
+ end
76
+
77
+ specify { parsed_data[:data].should == "Foo, it is page 2." }
78
+ end
79
+
80
+ context "parsing a request with the default parser" do
81
+ let(:parsed_data) { subject.request(:_method => :get, :_path => "users/1")[:parsed_data] }
82
+ before do
83
+ subject.setup :url => "https://api.example.com" do |builder|
84
+ builder.use Her::Middleware::FirstLevelParseJSON
85
+ builder.adapter :test do |stub|
86
+ stub.get("/users/1") { |env| [200, {}, MultiJson.dump({ :id => 1, :name => "George Michael Bluth", :errors => ["This is a single error"], :metadata => { :page => 1, :per_page => 10 } })] }
87
+ end
88
+ end
89
+ end
90
+
91
+ specify do
92
+ parsed_data[:data].should == { :id => 1, :name => "George Michael Bluth" }
93
+ parsed_data[:errors].should == ["This is a single error"]
94
+ parsed_data[:metadata].should == { :page => 1, :per_page => 10 }
95
+ end
96
+ end
97
+
98
+ context "parsing a request with a custom parser" do
99
+ let(:parsed_data) { subject.request(:_method => :get, :_path => "users/1")[:parsed_data] }
100
+ before do
101
+ class CustomParser < Faraday::Response::Middleware
102
+ def on_complete(env)
103
+ json = MultiJson.load(env[:body], :symbolize_keys => true)
104
+ errors = json.delete(:errors) || []
105
+ metadata = json.delete(:metadata) || {}
106
+ env[:body] = {
107
+ :data => json,
108
+ :errors => errors,
109
+ :metadata => metadata,
110
+ }
111
+ end
112
+ end
113
+
114
+ subject.setup :url => "https://api.example.com" do |builder|
115
+ builder.use CustomParser
116
+ builder.use Faraday::Request::UrlEncoded
117
+ builder.adapter :test do |stub|
118
+ stub.get("/users/1") { |env| [200, {}, MultiJson.dump(:id => 1, :name => "George Michael Bluth")] }
119
+ end
120
+ end
121
+ end
122
+
123
+ specify do
124
+ parsed_data[:data].should == { :id => 1, :name => "George Michael Bluth" }
125
+ parsed_data[:errors].should == []
126
+ parsed_data[:metadata].should == {}
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Her::Collection do
4
+
5
+ let(:items) { [1, 2, 3, 4] }
6
+ let(:metadata) { { :name => 'Testname' } }
7
+ let(:errors) { { :name => ['not_present'] } }
8
+
9
+ describe "#new" do
10
+ context "without parameters" do
11
+ subject { Her::Collection.new }
12
+
13
+ it { should eq([]) }
14
+ its(:metadata) { should eq({}) }
15
+ its(:errors) { should eq({}) }
16
+ end
17
+
18
+ context "with parameters" do
19
+ subject { Her::Collection.new(items, metadata, errors) }
20
+
21
+ it { should eq([1,2,3,4]) }
22
+ its(:metadata) { should eq({ :name => 'Testname' }) }
23
+ its(:errors) { should eq({ :name => ['not_present'] }) }
24
+ end
25
+ end
26
+ end
@@ -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,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,416 @@
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
+ end
136
+
137
+ let(:user_with_included_data_after_create) { Foo::User.create }
138
+ let(:user_with_included_data_after_save_existing) { Foo::User.save_existing(5, :name => "Clancy Brown") }
139
+ let(:user_with_included_data_after_destroy) { Foo::User.new(:id => 5).destroy }
140
+ let(:comment_without_included_parent_data) { Foo::Comment.new(:id => 7, :user_id => 1) }
141
+
142
+ it "maps an array of included data through has_many" do
143
+ @user_with_included_data.comments.first.should be_a(Foo::Comment)
144
+ @user_with_included_data.comments.length.should == 2
145
+ @user_with_included_data.comments.first.id.should == 2
146
+ @user_with_included_data.comments.first.body.should == "Tobias, you blow hard!"
147
+ end
148
+
149
+ it "does not refetch the parents models data if they have been fetched before" do
150
+ @user_with_included_data.comments.first.user.object_id.should == @user_with_included_data.object_id
151
+ end
152
+
153
+ it "does fetch the parent models data only once" do
154
+ comment_without_included_parent_data.user.object_id.should == comment_without_included_parent_data.user.object_id
155
+ end
156
+
157
+ it "does fetch the parent models data that was cached if called with parameters" do
158
+ comment_without_included_parent_data.user.object_id.should_not == comment_without_included_parent_data.user.where(:a => 2).object_id
159
+ end
160
+
161
+ it "uses the given inverse_of key to set the parent model" do
162
+ @user_with_included_data.posts.first.admin.object_id.should == @user_with_included_data.object_id
163
+ end
164
+
165
+ it "fetches data that was not included through has_many" do
166
+ @user_without_included_data.comments.first.should be_a(Foo::Comment)
167
+ @user_without_included_data.comments.length.should == 2
168
+ @user_without_included_data.comments.first.id.should == 4
169
+ @user_without_included_data.comments.first.body.should == "They're having a FIRESALE?"
170
+ end
171
+
172
+ it "fetches has_many data even if it was included, only if called with parameters" do
173
+ @user_with_included_data.comments.where(:foo_id => 1).length.should == 1
174
+ end
175
+
176
+ it "fetches data that was not included through has_many only once" do
177
+ @user_without_included_data.comments.first.object_id.should == @user_without_included_data.comments.first.object_id
178
+ end
179
+
180
+ it "fetches data that was cached through has_many if called with parameters" do
181
+ @user_without_included_data.comments.first.object_id.should_not == @user_without_included_data.comments.where(:foo_id => 1).first.object_id
182
+ end
183
+
184
+ it "maps an array of included data through has_one" do
185
+ @user_with_included_data.role.should be_a(Foo::Role)
186
+ @user_with_included_data.role.object_id.should == @user_with_included_data.role.object_id
187
+ @user_with_included_data.role.id.should == 1
188
+ @user_with_included_data.role.body.should == "Admin"
189
+ end
190
+
191
+ it "fetches data that was not included through has_one" do
192
+ @user_without_included_data.role.should be_a(Foo::Role)
193
+ @user_without_included_data.role.id.should == 2
194
+ @user_without_included_data.role.body.should == "User"
195
+ end
196
+
197
+ it "fetches has_one data even if it was included, only if called with parameters" do
198
+ @user_with_included_data.role.where(:foo_id => 2).id.should == 3
199
+ end
200
+
201
+ it "maps an array of included data through belongs_to" do
202
+ @user_with_included_data.organization.should be_a(Foo::Organization)
203
+ @user_with_included_data.organization.id.should == 1
204
+ @user_with_included_data.organization.name.should == "Bluth Company"
205
+ end
206
+
207
+ it "fetches data that was not included through belongs_to" do
208
+ @user_without_included_data.organization.should be_a(Foo::Organization)
209
+ @user_without_included_data.organization.id.should == 2
210
+ @user_without_included_data.organization.name.should == "Bluth Company"
211
+ end
212
+
213
+ it "fetches belongs_to data even if it was included, only if called with parameters" do
214
+ @user_with_included_data.organization.where(:foo_id => 1).name.should == "Bluth Company Foo"
215
+ end
216
+
217
+ it "can tell if it has a association" do
218
+ @user_without_included_data.has_association?(:unknown_association).should be false
219
+ @user_without_included_data.has_association?(:organization).should be true
220
+ end
221
+
222
+ it "fetches the resource corresponding to a named association" do
223
+ @user_without_included_data.get_association(:unknown_association).should be_nil
224
+ @user_without_included_data.get_association(:organization).name.should == "Bluth Company"
225
+ end
226
+
227
+ it "pass query string parameters when additional arguments are passed" do
228
+ @user_without_included_data.organization.where(:admin => true).name.should == "Bluth Company (admin)"
229
+ @user_without_included_data.organization.name.should == "Bluth Company"
230
+ end
231
+
232
+ it "fetches data with the specified id when calling find" do
233
+ comment = @user_without_included_data.comments.find(5)
234
+ comment.id.should eq(5)
235
+ end
236
+
237
+ it "'s associations responds to #empty?" do
238
+ @user_without_included_data.organization.respond_to?(:empty?).should be_truthy
239
+ @user_without_included_data.organization.should_not be_empty
240
+ end
241
+
242
+ it 'includes has_many relationships in params by default' do
243
+ params = @user_with_included_data.to_params
244
+ params[:comments].should be_kind_of(Array)
245
+ params[:comments].length.should eq(2)
246
+ end
247
+
248
+ [:create, :save_existing, :destroy].each do |type|
249
+ context "after #{type}" do
250
+ let(:subject) { self.send("user_with_included_data_after_#{type}")}
251
+
252
+ it "maps an array of included data through has_many" do
253
+ subject.comments.first.should be_a(Foo::Comment)
254
+ subject.comments.length.should == 1
255
+ subject.comments.first.id.should == 99
256
+ subject.comments.first.body.should == "Rodríguez, nasibisibusi?"
257
+ end
258
+
259
+ it "maps an array of included data through has_one" do
260
+ subject.role.should be_a(Foo::Role)
261
+ subject.role.id.should == 1
262
+ subject.role.body.should == "Admin"
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+ context "handling associations with details" do
269
+ before do
270
+ Her::API.setup :url => "https://api.example.com" do |builder|
271
+ builder.use Her::Middleware::FirstLevelParseJSON
272
+ builder.use Faraday::Request::UrlEncoded
273
+ builder.adapter :test do |stub|
274
+ stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :organization => { :id => 1, :name => "Bluth Company Inc." }, :organization_id => 1 }.to_json] }
275
+ stub.get("/users/4") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :organization => { :id => 1, :name => "Bluth Company Inc." } }.to_json] }
276
+ stub.get("/users/2") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :organization_id => 1 }.to_json] }
277
+ stub.get("/users/3") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :company => nil }.to_json] }
278
+ stub.get("/companies/1") { |env| [200, {}, { :id => 1, :name => "Bluth Company" }.to_json] }
279
+ end
280
+ end
281
+
282
+ spawn_model "Foo::User" do
283
+ belongs_to :company, :path => "/organizations/:id", :foreign_key => :organization_id, :data_key => :organization
284
+ end
285
+
286
+ spawn_model "Foo::Company"
287
+
288
+ @user_with_included_data = Foo::User.find(1)
289
+ @user_without_included_data = Foo::User.find(2)
290
+ @user_with_included_nil_data = Foo::User.find(3)
291
+ @user_with_included_data_but_no_fk = Foo::User.find(4)
292
+ end
293
+
294
+ it "maps an array of included data through belongs_to" do
295
+ @user_with_included_data.company.should be_a(Foo::Company)
296
+ @user_with_included_data.company.id.should == 1
297
+ @user_with_included_data.company.name.should == "Bluth Company Inc."
298
+ end
299
+
300
+ it "does not map included data if it’s nil" do
301
+ @user_with_included_nil_data.company.should be_nil
302
+ end
303
+
304
+ it "fetches data that was not included through belongs_to" do
305
+ @user_without_included_data.company.should be_a(Foo::Company)
306
+ @user_without_included_data.company.id.should == 1
307
+ @user_without_included_data.company.name.should == "Bluth Company"
308
+ end
309
+
310
+ it "does not require foreugn key to have nested object" do
311
+ @user_with_included_data_but_no_fk.company.name.should == "Bluth Company Inc."
312
+ end
313
+ end
314
+
315
+ context "object returned by the association method" do
316
+ before do
317
+ spawn_model "Foo::Role" do
318
+ def present?
319
+ "of_course"
320
+ end
321
+ end
322
+ spawn_model "Foo::User" do
323
+ has_one :role
324
+ end
325
+ end
326
+
327
+ let(:associated_value) { Foo::Role.new }
328
+ let(:user_with_role) do
329
+ Foo::User.new.tap { |user| user.role = associated_value }
330
+ end
331
+
332
+ subject { user_with_role.role }
333
+
334
+ it "doesnt mask the object's basic methods" do
335
+ subject.class.should == Foo::Role
336
+ end
337
+
338
+ it "doesnt mask core methods like extend" do
339
+ committer = Module.new
340
+ subject.extend committer
341
+ associated_value.should be_kind_of committer
342
+ end
343
+
344
+ it "can return the association object" do
345
+ subject.association.should be_kind_of Her::Model::Associations::Association
346
+ end
347
+
348
+ it "still can call fetch via the association" do
349
+ subject.association.fetch.should eq associated_value
350
+ end
351
+
352
+ it "calls missing methods on associated value" do
353
+ subject.present?.should == "of_course"
354
+ end
355
+
356
+ it "can use association methods like where" do
357
+ subject.where(role: 'committer').association.
358
+ params.should include :role
359
+ end
360
+ end
361
+
362
+ context "building and creating association data" do
363
+ before do
364
+ spawn_model "Foo::Comment"
365
+ spawn_model "Foo::User" do
366
+ has_many :comments
367
+ end
368
+ end
369
+
370
+ context "with #build" do
371
+ it "takes the parent primary key" do
372
+ @comment = Foo::User.new(:id => 10).comments.build(:body => "Hello!")
373
+ @comment.body.should == "Hello!"
374
+ @comment.user_id.should == 10
375
+ end
376
+ end
377
+
378
+ context "with #create" do
379
+ before do
380
+ Her::API.setup :url => "https://api.example.com" do |builder|
381
+ builder.use Her::Middleware::FirstLevelParseJSON
382
+ builder.use Faraday::Request::UrlEncoded
383
+ builder.adapter :test do |stub|
384
+ stub.get("/users/10") { |env| [200, {}, { :id => 10 }.to_json] }
385
+ 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] }
386
+ end
387
+ end
388
+
389
+ Foo::User.use_api Her::API.default_api
390
+ Foo::Comment.use_api Her::API.default_api
391
+ end
392
+
393
+ it "takes the parent primary key and saves the resource" do
394
+ @user = Foo::User.find(10)
395
+ @comment = @user.comments.create(:body => "Hello!")
396
+ @comment.id.should == 1
397
+ @comment.body.should == "Hello!"
398
+ @comment.user_id.should == 10
399
+ @user.comments.should == [@comment]
400
+ end
401
+ end
402
+
403
+ context "with #new" do
404
+ it "creates nested models from hash attibutes" do
405
+ user = Foo::User.new(:name => "vic", :comments => [{:text => "hello"}])
406
+ user.comments.first.text.should == "hello"
407
+ end
408
+
409
+ it "assigns nested models if given as already constructed objects" do
410
+ bye = Foo::Comment.new(:text => "goodbye")
411
+ user = Foo::User.new(:name => 'vic', :comments => [bye])
412
+ user.comments.first.text.should == 'goodbye'
413
+ end
414
+ end
415
+ end
416
+ end