herr 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +15 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +990 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +81 -0
  12. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  13. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  16. data/her.gemspec +30 -0
  17. data/lib/her.rb +16 -0
  18. data/lib/her/api.rb +115 -0
  19. data/lib/her/collection.rb +12 -0
  20. data/lib/her/errors.rb +27 -0
  21. data/lib/her/middleware.rb +10 -0
  22. data/lib/her/middleware/accept_json.rb +17 -0
  23. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  24. data/lib/her/middleware/parse_json.rb +21 -0
  25. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  26. data/lib/her/model.rb +72 -0
  27. data/lib/her/model/associations.rb +141 -0
  28. data/lib/her/model/associations/association.rb +103 -0
  29. data/lib/her/model/associations/association_proxy.rb +46 -0
  30. data/lib/her/model/associations/belongs_to_association.rb +96 -0
  31. data/lib/her/model/associations/has_many_association.rb +100 -0
  32. data/lib/her/model/associations/has_one_association.rb +79 -0
  33. data/lib/her/model/attributes.rb +266 -0
  34. data/lib/her/model/base.rb +33 -0
  35. data/lib/her/model/deprecated_methods.rb +61 -0
  36. data/lib/her/model/http.rb +114 -0
  37. data/lib/her/model/introspection.rb +65 -0
  38. data/lib/her/model/nested_attributes.rb +45 -0
  39. data/lib/her/model/orm.rb +205 -0
  40. data/lib/her/model/parse.rb +227 -0
  41. data/lib/her/model/paths.rb +121 -0
  42. data/lib/her/model/relation.rb +164 -0
  43. data/lib/her/version.rb +3 -0
  44. data/spec/api_spec.rb +131 -0
  45. data/spec/collection_spec.rb +26 -0
  46. data/spec/middleware/accept_json_spec.rb +10 -0
  47. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  48. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  49. data/spec/model/associations_spec.rb +416 -0
  50. data/spec/model/attributes_spec.rb +268 -0
  51. data/spec/model/callbacks_spec.rb +145 -0
  52. data/spec/model/dirty_spec.rb +86 -0
  53. data/spec/model/http_spec.rb +194 -0
  54. data/spec/model/introspection_spec.rb +76 -0
  55. data/spec/model/nested_attributes_spec.rb +134 -0
  56. data/spec/model/orm_spec.rb +479 -0
  57. data/spec/model/parse_spec.rb +373 -0
  58. data/spec/model/paths_spec.rb +341 -0
  59. data/spec/model/relation_spec.rb +226 -0
  60. data/spec/model/validations_spec.rb +42 -0
  61. data/spec/model_spec.rb +31 -0
  62. data/spec/spec_helper.rb +26 -0
  63. data/spec/support/extensions/array.rb +5 -0
  64. data/spec/support/extensions/hash.rb +5 -0
  65. data/spec/support/macros/her_macros.rb +17 -0
  66. data/spec/support/macros/model_macros.rb +29 -0
  67. data/spec/support/macros/request_macros.rb +27 -0
  68. metadata +280 -0
@@ -0,0 +1,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