her5 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +17 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +1017 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +101 -0
- data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
- data/gemfiles/Gemfile.activemodel-4.0 +7 -0
- data/gemfiles/Gemfile.activemodel-4.1 +7 -0
- data/gemfiles/Gemfile.activemodel-4.2 +7 -0
- data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
- data/her5.gemspec +30 -0
- data/lib/her.rb +19 -0
- data/lib/her/api.rb +120 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +104 -0
- data/lib/her/json_api/model.rb +57 -0
- data/lib/her/middleware.rb +12 -0
- data/lib/her/middleware/accept_json.rb +17 -0
- data/lib/her/middleware/first_level_parse_json.rb +36 -0
- data/lib/her/middleware/json_api_parser.rb +68 -0
- data/lib/her/middleware/parse_json.rb +28 -0
- data/lib/her/middleware/second_level_parse_json.rb +36 -0
- data/lib/her/model.rb +75 -0
- data/lib/her/model/associations.rb +141 -0
- data/lib/her/model/associations/association.rb +107 -0
- data/lib/her/model/associations/association_proxy.rb +45 -0
- data/lib/her/model/associations/belongs_to_association.rb +101 -0
- data/lib/her/model/associations/has_many_association.rb +101 -0
- data/lib/her/model/associations/has_one_association.rb +80 -0
- data/lib/her/model/attributes.rb +297 -0
- data/lib/her/model/base.rb +33 -0
- data/lib/her/model/deprecated_methods.rb +61 -0
- data/lib/her/model/http.rb +113 -0
- data/lib/her/model/introspection.rb +65 -0
- data/lib/her/model/nested_attributes.rb +84 -0
- data/lib/her/model/orm.rb +207 -0
- data/lib/her/model/parse.rb +221 -0
- data/lib/her/model/paths.rb +126 -0
- data/lib/her/model/relation.rb +164 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +114 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/json_api/model_spec.rb +305 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/json_api_parser_spec.rb +32 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +31 -0
- data/spec/model/associations_spec.rb +504 -0
- data/spec/model/attributes_spec.rb +389 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +91 -0
- data/spec/model/http_spec.rb +158 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +506 -0
- data/spec/model/parse_spec.rb +345 -0
- data/spec/model/paths_spec.rb +347 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +44 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/extensions/array.rb +5 -0
- data/spec/support/extensions/hash.rb +5 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +36 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +289 -0
@@ -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
|