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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +15 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +990 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +81 -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/her.gemspec +30 -0
- data/lib/her.rb +16 -0
- data/lib/her/api.rb +115 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +27 -0
- data/lib/her/middleware.rb +10 -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/parse_json.rb +21 -0
- data/lib/her/middleware/second_level_parse_json.rb +36 -0
- data/lib/her/model.rb +72 -0
- data/lib/her/model/associations.rb +141 -0
- data/lib/her/model/associations/association.rb +103 -0
- data/lib/her/model/associations/association_proxy.rb +46 -0
- data/lib/her/model/associations/belongs_to_association.rb +96 -0
- data/lib/her/model/associations/has_many_association.rb +100 -0
- data/lib/her/model/associations/has_one_association.rb +79 -0
- data/lib/her/model/attributes.rb +266 -0
- data/lib/her/model/base.rb +33 -0
- data/lib/her/model/deprecated_methods.rb +61 -0
- data/lib/her/model/http.rb +114 -0
- data/lib/her/model/introspection.rb +65 -0
- data/lib/her/model/nested_attributes.rb +45 -0
- data/lib/her/model/orm.rb +205 -0
- data/lib/her/model/parse.rb +227 -0
- data/lib/her/model/paths.rb +121 -0
- data/lib/her/model/relation.rb +164 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +131 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations_spec.rb +416 -0
- data/spec/model/attributes_spec.rb +268 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +86 -0
- data/spec/model/http_spec.rb +194 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +479 -0
- data/spec/model/parse_spec.rb +373 -0
- data/spec/model/paths_spec.rb +341 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +31 -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 +29 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +280 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.join(File.dirname(__FILE__), "../spec_helper.rb")
|
3
|
+
|
4
|
+
describe Her::Model::HTTP do
|
5
|
+
context "binding a model with an API" do
|
6
|
+
let(:api1) { Her::API.new :url => "https://api1.example.com" }
|
7
|
+
let(:api2) { Her::API.new :url => "https://api2.example.com" }
|
8
|
+
|
9
|
+
before do
|
10
|
+
spawn_model("Foo::User")
|
11
|
+
spawn_model("Foo::Comment")
|
12
|
+
Her::API.setup :url => "https://api.example.com"
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when binding a model to an instance of Her::API" do
|
16
|
+
before { Foo::User.uses_api api1 }
|
17
|
+
subject { Foo::User.her_api }
|
18
|
+
its(:base_uri) { should == "https://api1.example.com" }
|
19
|
+
end
|
20
|
+
|
21
|
+
context "when binding a model directly to Her::API" do
|
22
|
+
before { spawn_model "Foo::User" }
|
23
|
+
subject { Foo::User.her_api }
|
24
|
+
its(:base_uri) { should == "https://api.example.com" }
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when using a proc for uses_api" do
|
28
|
+
before do
|
29
|
+
Foo::User.uses_api lambda { Her::API.new :url => 'http://api-lambda.example.com' }
|
30
|
+
end
|
31
|
+
|
32
|
+
specify { Foo::User.her_api.base_uri.should == 'http://api-lambda.example.com' }
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when binding two models to two different instances of Her::API" do
|
36
|
+
before do
|
37
|
+
Foo::User.uses_api api1
|
38
|
+
Foo::Comment.uses_api api2
|
39
|
+
end
|
40
|
+
|
41
|
+
specify { Foo::User.her_api.base_uri.should == "https://api1.example.com" }
|
42
|
+
specify { Foo::Comment.her_api.base_uri.should == "https://api2.example.com" }
|
43
|
+
end
|
44
|
+
|
45
|
+
context "binding one model to Her::API and another one to an instance of Her::API" do
|
46
|
+
before { Foo::Comment.uses_api api2 }
|
47
|
+
specify { Foo::User.her_api.base_uri.should == "https://api.example.com" }
|
48
|
+
specify { Foo::Comment.her_api.base_uri.should == "https://api2.example.com" }
|
49
|
+
end
|
50
|
+
|
51
|
+
context "when binding a model to its superclass' her_api" do
|
52
|
+
before do
|
53
|
+
spawn_model "Foo::Superclass"
|
54
|
+
Foo::Superclass.uses_api api1
|
55
|
+
Foo::Subclass = Class.new(Foo::Superclass)
|
56
|
+
end
|
57
|
+
|
58
|
+
specify { Foo::Subclass.her_api.should == Foo::Superclass.her_api }
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when changing her_api without changing the parent class' her_api" do
|
62
|
+
before do
|
63
|
+
spawn_model "Foo::Superclass"
|
64
|
+
Foo::Subclass = Class.new(Foo::Superclass)
|
65
|
+
Foo::Superclass.uses_api api1
|
66
|
+
Foo::Subclass.uses_api api2
|
67
|
+
end
|
68
|
+
|
69
|
+
specify { Foo::Subclass.her_api.should_not == Foo::Superclass.her_api }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "making HTTP requests" do
|
74
|
+
before do
|
75
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
76
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
77
|
+
builder.use Faraday::Request::UrlEncoded
|
78
|
+
builder.adapter :test do |stub|
|
79
|
+
stub.get("/users") { |env| [200, {}, [{ :id => 1 }].to_json] }
|
80
|
+
stub.get("/users/1") { |env| [200, {}, { :id => 1 }.to_json] }
|
81
|
+
stub.get("/users/popular") do |env|
|
82
|
+
if env[:params]["page"] == "2"
|
83
|
+
[200, {}, [{ :id => 3 }, { :id => 4 }].to_json]
|
84
|
+
else
|
85
|
+
[200, {}, [{ :id => 1 }, { :id => 2 }].to_json]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
spawn_model "Foo::User"
|
92
|
+
end
|
93
|
+
|
94
|
+
describe :get do
|
95
|
+
subject { Foo::User.get(:popular) }
|
96
|
+
its(:length) { should == 2 }
|
97
|
+
specify { subject.first.id.should == 1 }
|
98
|
+
end
|
99
|
+
|
100
|
+
describe :get_raw do
|
101
|
+
context "with a block" do
|
102
|
+
specify do
|
103
|
+
Foo::User.get_raw("/users") do |parsed_data, response|
|
104
|
+
parsed_data[:data].should == [{ :id => 1 }]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "with a return value" do
|
110
|
+
subject { Foo::User.get_raw("/users") }
|
111
|
+
specify { subject[:parsed_data][:data].should == [{ :id => 1 }] }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe :get_collection do
|
116
|
+
context "with a String path" do
|
117
|
+
subject { Foo::User.get_collection("/users/popular") }
|
118
|
+
its(:length) { should == 2 }
|
119
|
+
specify { subject.first.id.should == 1 }
|
120
|
+
end
|
121
|
+
|
122
|
+
context "with a Symbol" do
|
123
|
+
subject { Foo::User.get_collection(:popular) }
|
124
|
+
its(:length) { should == 2 }
|
125
|
+
specify { subject.first.id.should == 1 }
|
126
|
+
end
|
127
|
+
|
128
|
+
context "with extra parameters" do
|
129
|
+
subject { Foo::User.get_collection(:popular, :page => 2) }
|
130
|
+
its(:length) { should == 2 }
|
131
|
+
specify { subject.first.id.should == 3 }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe :get_resource do
|
136
|
+
context "with a String path" do
|
137
|
+
subject { Foo::User.get_resource("/users/1") }
|
138
|
+
its(:id) { should == 1 }
|
139
|
+
end
|
140
|
+
|
141
|
+
context "with a Symbol" do
|
142
|
+
subject { Foo::User.get_resource(:"1") }
|
143
|
+
its(:id) { should == 1 }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe :get_raw do
|
148
|
+
specify do
|
149
|
+
Foo::User.get_raw(:popular) do |parsed_data, response|
|
150
|
+
parsed_data[:data].should == [{ :id => 1 }, { :id => 2 }]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "setting custom HTTP requests" do
|
157
|
+
before do
|
158
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
159
|
+
connection.use Her::Middleware::FirstLevelParseJSON
|
160
|
+
connection.adapter :test do |stub|
|
161
|
+
stub.get("/users/popular") { |env| [200, {}, [{ :id => 1 }, { :id => 2 }].to_json] }
|
162
|
+
stub.post("/users/from_default") { |env| [200, {}, { :id => 4 }.to_json] }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
spawn_model "Foo::User"
|
167
|
+
end
|
168
|
+
|
169
|
+
subject { Foo::User }
|
170
|
+
|
171
|
+
describe :custom_get do
|
172
|
+
context "without cache" do
|
173
|
+
before { Foo::User.custom_get :popular, :recent }
|
174
|
+
it { should respond_to(:popular) }
|
175
|
+
it { should respond_to(:recent) }
|
176
|
+
|
177
|
+
context "making the HTTP request" do
|
178
|
+
subject { Foo::User.popular }
|
179
|
+
its(:length) { should == 2 }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe :custom_post do
|
185
|
+
before { Foo::User.custom_post :from_default }
|
186
|
+
it { should respond_to(:from_default) }
|
187
|
+
|
188
|
+
context "making the HTTP request" do
|
189
|
+
subject { Foo::User.from_default(:name => "Tobias Fünke") }
|
190
|
+
its(:id) { should == 4 }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.join(File.dirname(__FILE__), "../spec_helper.rb")
|
3
|
+
|
4
|
+
describe Her::Model::Introspection do
|
5
|
+
context "introspecting a resource" 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.post("/users") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
|
12
|
+
stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
|
13
|
+
stub.put("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
|
14
|
+
stub.delete("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Funke" }.to_json] }
|
15
|
+
stub.get("/projects/1/comments") { |env| [200, {}, [{ :id => 1, :body => "Hello!" }].to_json] }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
spawn_model "Foo::User"
|
20
|
+
spawn_model "Foo::Comment" do
|
21
|
+
collection_path "projects/:project_id/comments"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#inspect" do
|
26
|
+
it "outputs resource attributes for an existing resource" do
|
27
|
+
@user = Foo::User.find(1)
|
28
|
+
["#<Foo::User(users/1) name=\"Tobias Funke\" id=1>", "#<Foo::User(users/1) id=1 name=\"Tobias Funke\">"].should include(@user.inspect)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "outputs resource attributes for an not-saved-yet resource" do
|
32
|
+
@user = Foo::User.new(:name => "Tobias Funke")
|
33
|
+
@user.inspect.should == "#<Foo::User(users) name=\"Tobias Funke\">"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "outputs resource attributes using getters" do
|
37
|
+
@user = Foo::User.new(:name => "Tobias Funke", :password => "Funke")
|
38
|
+
@user.instance_eval {def password; 'filtered'; end}
|
39
|
+
@user.inspect.should include("name=\"Tobias Funke\"")
|
40
|
+
@user.inspect.should include("password=\"filtered\"")
|
41
|
+
@user.inspect.should_not include("password=\"Funke\"")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "support dash on attribute" do
|
45
|
+
@user = Foo::User.new(:'life-span' => "3 years")
|
46
|
+
@user.inspect.should include("life-span=\"3 years\"")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#inspect with errors in resource path" do
|
51
|
+
it "prints the resource path as “unknown”" do
|
52
|
+
@comment = Foo::Comment.where(:project_id => 1).first
|
53
|
+
path = '<unknown path, missing `project_id`>'
|
54
|
+
["#<Foo::Comment(#{path}) body=\"Hello!\" id=1>", "#<Foo::Comment(#{path}) id=1 body=\"Hello!\">"].should include(@comment.inspect)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#her_nearby_class" do
|
60
|
+
context "for a class inside of a module" do
|
61
|
+
before do
|
62
|
+
spawn_model "Foo::User"
|
63
|
+
spawn_model "Foo::AccessRecord"
|
64
|
+
spawn_model "AccessRecord"
|
65
|
+
spawn_model "Log"
|
66
|
+
end
|
67
|
+
|
68
|
+
it "returns a sibling class, if found" do
|
69
|
+
Foo::User.her_nearby_class("AccessRecord").should == Foo::AccessRecord
|
70
|
+
AccessRecord.her_nearby_class("Log").should == Log
|
71
|
+
Foo::User.her_nearby_class("Log").should == Log
|
72
|
+
Foo::User.her_nearby_class("X").should be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.join(File.dirname(__FILE__), "../spec_helper.rb")
|
3
|
+
|
4
|
+
describe Her::Model::NestedAttributes do
|
5
|
+
context "with a belongs_to association" 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
|
+
end
|
11
|
+
|
12
|
+
spawn_model "Foo::User" do
|
13
|
+
belongs_to :company, :path => "/organizations/:id", :foreign_key => :organization_id
|
14
|
+
accepts_nested_attributes_for :company
|
15
|
+
end
|
16
|
+
|
17
|
+
spawn_model "Foo::Company"
|
18
|
+
|
19
|
+
@user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :company_attributes => { :name => "Example Company" }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "when child does not yet exist" do
|
23
|
+
it "creates an instance of the associated class" do
|
24
|
+
@user_with_data_through_nested_attributes.company.should be_a(Foo::Company)
|
25
|
+
@user_with_data_through_nested_attributes.company.name.should == "Example Company"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "when child does exist" do
|
30
|
+
it "updates the attributes of the associated object" do
|
31
|
+
@user_with_data_through_nested_attributes.company_attributes = { :name => "Fünke's Company" }
|
32
|
+
@user_with_data_through_nested_attributes.company.should be_a(Foo::Company)
|
33
|
+
@user_with_data_through_nested_attributes.company.name.should == "Fünke's Company"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with a has_one association" do
|
39
|
+
before do
|
40
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
41
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
42
|
+
builder.use Faraday::Request::UrlEncoded
|
43
|
+
end
|
44
|
+
|
45
|
+
spawn_model "Foo::User" do
|
46
|
+
has_one :pet
|
47
|
+
accepts_nested_attributes_for :pet
|
48
|
+
end
|
49
|
+
|
50
|
+
spawn_model "Foo::Pet"
|
51
|
+
|
52
|
+
@user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :pet_attributes => { :name => "Hasi" }
|
53
|
+
end
|
54
|
+
|
55
|
+
context "when child does not yet exist" do
|
56
|
+
it "creates an instance of the associated class" do
|
57
|
+
@user_with_data_through_nested_attributes.pet.should be_a(Foo::Pet)
|
58
|
+
@user_with_data_through_nested_attributes.pet.name.should == "Hasi"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when child does exist" do
|
63
|
+
it "updates the attributes of the associated object" do
|
64
|
+
@user_with_data_through_nested_attributes.pet_attributes = { :name => "Rodriguez" }
|
65
|
+
@user_with_data_through_nested_attributes.pet.should be_a(Foo::Pet)
|
66
|
+
@user_with_data_through_nested_attributes.pet.name.should == "Rodriguez"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "with a has_many association" do
|
72
|
+
before do
|
73
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
74
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
75
|
+
builder.use Faraday::Request::UrlEncoded
|
76
|
+
end
|
77
|
+
|
78
|
+
spawn_model "Foo::User" do
|
79
|
+
has_many :pets
|
80
|
+
accepts_nested_attributes_for :pets
|
81
|
+
end
|
82
|
+
|
83
|
+
spawn_model "Foo::Pet"
|
84
|
+
|
85
|
+
@user_with_data_through_nested_attributes = Foo::User.new :name => "Test", :pets_attributes => [{ :name => "Hasi" }, { :name => "Rodriguez" }]
|
86
|
+
end
|
87
|
+
|
88
|
+
context "when children do not yet exist" do
|
89
|
+
it "creates an instance of the associated class" do
|
90
|
+
@user_with_data_through_nested_attributes.pets.length.should == 2
|
91
|
+
@user_with_data_through_nested_attributes.pets[0].should be_a(Foo::Pet)
|
92
|
+
@user_with_data_through_nested_attributes.pets[1].should be_a(Foo::Pet)
|
93
|
+
@user_with_data_through_nested_attributes.pets[0].name.should == "Hasi"
|
94
|
+
@user_with_data_through_nested_attributes.pets[1].name.should == "Rodriguez"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "with a has_many association as a Hash" do
|
100
|
+
before do
|
101
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
102
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
103
|
+
builder.use Faraday::Request::UrlEncoded
|
104
|
+
end
|
105
|
+
|
106
|
+
spawn_model "Foo::User" do
|
107
|
+
has_many :pets
|
108
|
+
accepts_nested_attributes_for :pets
|
109
|
+
end
|
110
|
+
|
111
|
+
spawn_model "Foo::Pet"
|
112
|
+
|
113
|
+
@user_with_data_through_nested_attributes_as_hash = Foo::User.new :name => "Test", :pets_attributes => { '0' => { :name => "Hasi" }, '1' => { :name => "Rodriguez" }}
|
114
|
+
end
|
115
|
+
|
116
|
+
context "when children do not yet exist" do
|
117
|
+
it "creates an instance of the associated class" do
|
118
|
+
@user_with_data_through_nested_attributes_as_hash.pets.length.should == 2
|
119
|
+
@user_with_data_through_nested_attributes_as_hash.pets[0].should be_a(Foo::Pet)
|
120
|
+
@user_with_data_through_nested_attributes_as_hash.pets[1].should be_a(Foo::Pet)
|
121
|
+
@user_with_data_through_nested_attributes_as_hash.pets[0].name.should == "Hasi"
|
122
|
+
@user_with_data_through_nested_attributes_as_hash.pets[1].name.should == "Rodriguez"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context "with an unknown association" do
|
128
|
+
it "raises an error" do
|
129
|
+
expect {
|
130
|
+
spawn_model("Foo::User") { accepts_nested_attributes_for :company }
|
131
|
+
}.to raise_error(Her::Errors::AssociationUnknownError, 'Unknown association name :company')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,479 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.join(File.dirname(__FILE__), "../spec_helper.rb")
|
3
|
+
|
4
|
+
describe Her::Model::ORM do
|
5
|
+
context "mapping data to Ruby objects" do
|
6
|
+
before do
|
7
|
+
api = Her::API.new
|
8
|
+
api.setup :url => "https://api.example.com" do |builder|
|
9
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
10
|
+
builder.use Faraday::Request::UrlEncoded
|
11
|
+
builder.adapter :test do |stub|
|
12
|
+
stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke" }.to_json] }
|
13
|
+
stub.get("/users") { |env| [200, {}, [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }].to_json] }
|
14
|
+
stub.get("/admin_users") { |env| [200, {}, [{ :admin_id => 1, :name => "Tobias Fünke" }, { :admin_id => 2, :name => "Lindsay Fünke" }].to_json] }
|
15
|
+
stub.get("/admin_users/1") { |env| [200, {}, { :admin_id => 1, :name => "Tobias Fünke" }.to_json] }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
spawn_model "Foo::User" do
|
20
|
+
uses_api api
|
21
|
+
end
|
22
|
+
|
23
|
+
spawn_model "Foo::AdminUser" do
|
24
|
+
uses_api api
|
25
|
+
primary_key :admin_id
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "maps a single resource to a Ruby object" do
|
30
|
+
@user = Foo::User.find(1)
|
31
|
+
@user.id.should == 1
|
32
|
+
@user.name.should == "Tobias Fünke"
|
33
|
+
|
34
|
+
@admin = Foo::AdminUser.find(1)
|
35
|
+
@admin.id.should == 1
|
36
|
+
@admin.name.should == "Tobias Fünke"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "maps a collection of resources to an array of Ruby objects" do
|
40
|
+
@users = Foo::User.all
|
41
|
+
@users.length.should == 2
|
42
|
+
@users.first.name.should == "Tobias Fünke"
|
43
|
+
|
44
|
+
@users = Foo::AdminUser.all
|
45
|
+
@users.length.should == 2
|
46
|
+
@users.first.name.should == "Tobias Fünke"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "handles new resource" do
|
50
|
+
@new_user = Foo::User.new(:fullname => "Tobias Fünke")
|
51
|
+
@new_user.new?.should be_truthy
|
52
|
+
@new_user.fullname.should == "Tobias Fünke"
|
53
|
+
|
54
|
+
@existing_user = Foo::User.find(1)
|
55
|
+
@existing_user.new?.should be_falsey
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'handles new resource with custom primary key' do
|
59
|
+
@new_user = Foo::AdminUser.new(:fullname => 'Lindsay Fünke', :id => -1)
|
60
|
+
@new_user.should be_new
|
61
|
+
|
62
|
+
@existing_user = Foo::AdminUser.find(1)
|
63
|
+
@existing_user.should_not be_new
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "mapping data, metadata and error data to Ruby objects" do
|
68
|
+
before do
|
69
|
+
api = Her::API.new
|
70
|
+
api.setup :url => "https://api.example.com" do |builder|
|
71
|
+
builder.use Her::Middleware::SecondLevelParseJSON
|
72
|
+
builder.use Faraday::Request::UrlEncoded
|
73
|
+
builder.adapter :test do |stub|
|
74
|
+
stub.get("/users") { |env| [200, {}, { :data => [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }], :metadata => { :total_pages => 10, :next_page => 2 }, :errors => ["Oh", "My", "God"] }.to_json] }
|
75
|
+
stub.post("/users") { |env| [200, {}, { :data => { :name => "George Michael Bluth" }, :metadata => { :foo => "bar" }, :errors => ["Yes", "Sir"] }.to_json] }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
spawn_model :User do
|
80
|
+
uses_api api
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it "handles metadata on a collection" do
|
85
|
+
@users = User.all
|
86
|
+
@users.metadata[:total_pages].should == 10
|
87
|
+
end
|
88
|
+
|
89
|
+
it "handles error data on a collection" do
|
90
|
+
@users = User.all
|
91
|
+
@users.errors.length.should == 3
|
92
|
+
end
|
93
|
+
|
94
|
+
it "handles metadata on a resource" do
|
95
|
+
@user = User.create(:name => "George Michael Bluth")
|
96
|
+
@user.metadata[:foo].should == "bar"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "handles error data on a resource" do
|
100
|
+
@user = User.create(:name => "George Michael Bluth")
|
101
|
+
@user.response_errors.should == ["Yes", "Sir"]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "mapping data, metadata and error data in string keys to Ruby objects" do
|
106
|
+
before do
|
107
|
+
api = Her::API.new
|
108
|
+
api.setup :url => "https://api.example.com" do |builder|
|
109
|
+
builder.use Her::Middleware::SecondLevelParseJSON
|
110
|
+
builder.use Faraday::Request::UrlEncoded
|
111
|
+
builder.adapter :test do |stub|
|
112
|
+
stub.get("/users") { |env| [200, {}, { 'data' => [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }], 'metadata' => { :total_pages => 10, :next_page => 2 }, 'errors' => ["Oh", "My", "God"] }.to_json] }
|
113
|
+
stub.post("/users") { |env| [200, {}, { 'data' => { :name => "George Michael Bluth" }, 'metadata' => { :foo => "bar" }, 'errors' => ["Yes", "Sir"] }.to_json] }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
spawn_model :User do
|
118
|
+
uses_api api
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
it "handles metadata on a collection" do
|
123
|
+
@users = User.all
|
124
|
+
@users.metadata[:total_pages].should == 10
|
125
|
+
end
|
126
|
+
|
127
|
+
it "handles error data on a collection" do
|
128
|
+
@users = User.all
|
129
|
+
@users.errors.length.should == 3
|
130
|
+
end
|
131
|
+
|
132
|
+
it "handles metadata on a resource" do
|
133
|
+
@user = User.create(:name => "George Michael Bluth")
|
134
|
+
@user.metadata[:foo].should == "bar"
|
135
|
+
end
|
136
|
+
|
137
|
+
it "handles error data on a resource" do
|
138
|
+
@user = User.create(:name => "George Michael Bluth")
|
139
|
+
@user.response_errors.should == ["Yes", "Sir"]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "defining custom getters and setters" do
|
144
|
+
before do
|
145
|
+
api = Her::API.new
|
146
|
+
api.setup :url => "https://api.example.com" do |builder|
|
147
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
148
|
+
builder.use Faraday::Request::UrlEncoded
|
149
|
+
builder.adapter :test do |stub|
|
150
|
+
stub.get("/users/1") { |env| [200, {}, { :id => 1, :friends => ["Maeby", "GOB", "Anne"] }.to_json] }
|
151
|
+
stub.get("/users/2") { |env| [200, {}, { :id => 1 }.to_json] }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
spawn_model :User do
|
156
|
+
uses_api api
|
157
|
+
belongs_to :organization
|
158
|
+
|
159
|
+
def friends=(val)
|
160
|
+
val = val.gsub("\r", "").split("\n").map { |friend| friend.gsub(/^\s*\*\s*/, "") } if val and val.is_a?(String)
|
161
|
+
@attributes[:friends] = val
|
162
|
+
end
|
163
|
+
|
164
|
+
def friends
|
165
|
+
@attributes[:friends].map { |friend| "* #{friend}" }.join("\n")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it "handles custom setters" do
|
171
|
+
@user = User.find(1)
|
172
|
+
@user.friends.should == "* Maeby\n* GOB\n* Anne"
|
173
|
+
@user.instance_eval do
|
174
|
+
@attributes[:friends] = ["Maeby", "GOB", "Anne"]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
it "handles custom getters" do
|
179
|
+
@user = User.new
|
180
|
+
@user.friends = "* George\n* Oscar\n* Lucille"
|
181
|
+
@user.friends.should == "* George\n* Oscar\n* Lucille"
|
182
|
+
@user.instance_eval do
|
183
|
+
@attributes[:friends] = ["George", "Oscar", "Lucille"]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
context "finding resources" do
|
189
|
+
before do
|
190
|
+
api = Her::API.new
|
191
|
+
api.setup :url => "https://api.example.com" do |builder|
|
192
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
193
|
+
builder.use Faraday::Request::UrlEncoded
|
194
|
+
builder.adapter :test do |stub|
|
195
|
+
stub.get("/users/1") { |env| [200, {}, { :id => 1, :age => 42 }.to_json] }
|
196
|
+
stub.get("/users/2") { |env| [200, {}, { :id => 2, :age => 34 }.to_json] }
|
197
|
+
stub.get("/users?id[]=1&id[]=2") { |env| [200, {}, [{ :id => 1, :age => 42 }, { :id => 2, :age => 34 }].to_json] }
|
198
|
+
stub.get("/users?age=42&foo=bar") { |env| [200, {}, [{ :id => 3, :age => 42 }].to_json] }
|
199
|
+
stub.get("/users?age=42") { |env| [200, {}, [{ :id => 1, :age => 42 }].to_json] }
|
200
|
+
stub.get("/users?age=40") { |env| [200, {}, [{ :id => 1, :age => 40 }].to_json] }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
spawn_model :User do
|
205
|
+
uses_api api
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
it "handles finding by a single id" do
|
210
|
+
@user = User.find(1)
|
211
|
+
@user.id.should == 1
|
212
|
+
end
|
213
|
+
|
214
|
+
it "handles finding by multiple ids" do
|
215
|
+
@users = User.find(1, 2)
|
216
|
+
@users.should be_kind_of(Array)
|
217
|
+
@users.length.should == 2
|
218
|
+
@users[0].id.should == 1
|
219
|
+
@users[1].id.should == 2
|
220
|
+
end
|
221
|
+
|
222
|
+
it "handles finding by an array of ids" do
|
223
|
+
@users = User.find([1, 2])
|
224
|
+
@users.should be_kind_of(Array)
|
225
|
+
@users.length.should == 2
|
226
|
+
@users[0].id.should == 1
|
227
|
+
@users[1].id.should == 2
|
228
|
+
end
|
229
|
+
|
230
|
+
it "handles finding by an array of ids of length 1" do
|
231
|
+
@users = User.find([1])
|
232
|
+
@users.should be_kind_of(Array)
|
233
|
+
@users.length.should == 1
|
234
|
+
@users[0].id.should == 1
|
235
|
+
end
|
236
|
+
|
237
|
+
it "handles finding by an array id param of length 2" do
|
238
|
+
@users = User.find(id: [1, 2])
|
239
|
+
@users.should be_kind_of(Array)
|
240
|
+
@users.length.should == 2
|
241
|
+
@users[0].id.should == 1
|
242
|
+
@users[1].id.should == 2
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'handles finding with id parameter as an array' do
|
246
|
+
@users = User.where(id: [1, 2])
|
247
|
+
@users.should be_kind_of(Array)
|
248
|
+
@users.length.should == 2
|
249
|
+
@users[0].id.should == 1
|
250
|
+
@users[1].id.should == 2
|
251
|
+
end
|
252
|
+
|
253
|
+
it "handles finding with other parameters" do
|
254
|
+
@users = User.where(:age => 42, :foo => "bar").all
|
255
|
+
@users.should be_kind_of(Array)
|
256
|
+
@users.first.id.should == 3
|
257
|
+
end
|
258
|
+
|
259
|
+
it "handles finding with other parameters and scoped" do
|
260
|
+
@users = User.scoped
|
261
|
+
@users.where(:age => 42).should be_all { |u| u.age == 42 }
|
262
|
+
@users.where(:age => 40).should be_all { |u| u.age == 40 }
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
context "building resources" do
|
267
|
+
context "when request_new_object_on_build is not set (default)" do
|
268
|
+
before do
|
269
|
+
spawn_model("Foo::User")
|
270
|
+
end
|
271
|
+
|
272
|
+
it "builds a new resource without requesting it" do
|
273
|
+
Foo::User.should_not_receive(:request)
|
274
|
+
@new_user = Foo::User.build(:fullname => "Tobias Fünke")
|
275
|
+
@new_user.new?.should be_truthy
|
276
|
+
@new_user.fullname.should == "Tobias Fünke"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
context "when request_new_object_on_build is set" do
|
281
|
+
before do
|
282
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
283
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
284
|
+
builder.use Faraday::Request::UrlEncoded
|
285
|
+
builder.adapter :test do |stub|
|
286
|
+
stub.get("/users/new") { |env| ok! :id => nil, :fullname => params(env)[:fullname], :email => "tobias@bluthcompany.com" }
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
spawn_model("Foo::User") { request_new_object_on_build true }
|
291
|
+
end
|
292
|
+
|
293
|
+
it "requests a new resource" do
|
294
|
+
Foo::User.should_receive(:request).once.and_call_original
|
295
|
+
@new_user = Foo::User.build(:fullname => "Tobias Fünke")
|
296
|
+
@new_user.new?.should be_truthy
|
297
|
+
@new_user.fullname.should == "Tobias Fünke"
|
298
|
+
@new_user.email.should == "tobias@bluthcompany.com"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
context "creating resources" do
|
304
|
+
before do
|
305
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
306
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
307
|
+
builder.use Faraday::Request::UrlEncoded
|
308
|
+
builder.adapter :test do |stub|
|
309
|
+
stub.post("/users") { |env| [200, {}, { :id => 1, :fullname => Faraday::Utils.parse_query(env[:body])['fullname'], :email => Faraday::Utils.parse_query(env[:body])['email'] }.to_json] }
|
310
|
+
stub.post("/companies") { |env| [200, {}, { :errors => ["name is required"] }.to_json] }
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
spawn_model "Foo::User"
|
315
|
+
spawn_model "Foo::Company"
|
316
|
+
end
|
317
|
+
|
318
|
+
it "handle one-line resource creation" do
|
319
|
+
@user = Foo::User.create(:fullname => "Tobias Fünke", :email => "tobias@bluth.com")
|
320
|
+
@user.id.should == 1
|
321
|
+
@user.fullname.should == "Tobias Fünke"
|
322
|
+
@user.email.should == "tobias@bluth.com"
|
323
|
+
end
|
324
|
+
|
325
|
+
it "handle resource creation through Model.new + #save" do
|
326
|
+
@user = Foo::User.new(:fullname => "Tobias Fünke")
|
327
|
+
@user.save.should be_truthy
|
328
|
+
@user.fullname.should == "Tobias Fünke"
|
329
|
+
end
|
330
|
+
|
331
|
+
it "handle resource creation through Model.new + #save!" do
|
332
|
+
@user = Foo::User.new(:fullname => "Tobias Fünke")
|
333
|
+
@user.save!.should be_truthy
|
334
|
+
@user.fullname.should == "Tobias Fünke"
|
335
|
+
end
|
336
|
+
|
337
|
+
it "returns false when #save gets errors" do
|
338
|
+
@company = Foo::Company.new
|
339
|
+
@company.save.should be_falsey
|
340
|
+
end
|
341
|
+
|
342
|
+
it "raises ResourceInvalid when #save! gets errors" do
|
343
|
+
@company = Foo::Company.new
|
344
|
+
expect { @company.save! }.to raise_error Her::Errors::ResourceInvalid, "Remote validation failed: name is required"
|
345
|
+
end
|
346
|
+
|
347
|
+
it "don't overwrite data if response is empty" do
|
348
|
+
@company = Foo::Company.new(:name => 'Company Inc.')
|
349
|
+
@company.save.should be_falsey
|
350
|
+
@company.name.should == "Company Inc."
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
context "updating resources" do
|
355
|
+
before do
|
356
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
357
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
358
|
+
builder.use Faraday::Request::UrlEncoded
|
359
|
+
builder.adapter :test do |stub|
|
360
|
+
stub.get("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Tobias Fünke" }.to_json] }
|
361
|
+
stub.put("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Lindsay Fünke" }.to_json] }
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
spawn_model "Foo::User"
|
366
|
+
end
|
367
|
+
|
368
|
+
it "handle resource data update without saving it" do
|
369
|
+
@user = Foo::User.find(1)
|
370
|
+
@user.fullname.should == "Tobias Fünke"
|
371
|
+
@user.fullname = "Kittie Sanchez"
|
372
|
+
@user.fullname.should == "Kittie Sanchez"
|
373
|
+
end
|
374
|
+
|
375
|
+
it "handle resource update through the .update class method" do
|
376
|
+
@user = Foo::User.save_existing(1, { :fullname => "Lindsay Fünke" })
|
377
|
+
@user.fullname.should == "Lindsay Fünke"
|
378
|
+
end
|
379
|
+
|
380
|
+
it "handle resource update through #save on an existing resource" do
|
381
|
+
@user = Foo::User.find(1)
|
382
|
+
@user.fullname = "Lindsay Fünke"
|
383
|
+
@user.save
|
384
|
+
@user.fullname.should == "Lindsay Fünke"
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
context "deleting resources" do
|
389
|
+
before do
|
390
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
391
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
392
|
+
builder.use Faraday::Request::UrlEncoded
|
393
|
+
builder.adapter :test do |stub|
|
394
|
+
stub.get("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Tobias Fünke", :active => true }.to_json] }
|
395
|
+
stub.delete("/users/1") { |env| [200, {}, { :id => 1, :fullname => "Lindsay Fünke", :active => false }.to_json] }
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
spawn_model "Foo::User"
|
400
|
+
end
|
401
|
+
|
402
|
+
it "handle resource deletion through the .destroy class method" do
|
403
|
+
@user = Foo::User.destroy_existing(1)
|
404
|
+
@user.active.should be_falsey
|
405
|
+
@user.should be_destroyed
|
406
|
+
end
|
407
|
+
|
408
|
+
it "handle resource deletion through #destroy on an existing resource" do
|
409
|
+
@user = Foo::User.find(1)
|
410
|
+
@user.destroy
|
411
|
+
@user.active.should be_falsey
|
412
|
+
@user.should be_destroyed
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
context 'customizing HTTP methods' do
|
417
|
+
before do
|
418
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
419
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
420
|
+
builder.use Faraday::Request::UrlEncoded
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
context 'create' do
|
425
|
+
before do
|
426
|
+
Her::API.default_api.connection.adapter :test do |stub|
|
427
|
+
stub.put('/users') { |env| [200, {}, { :id => 1, :fullname => 'Tobias Fünke' }.to_json] }
|
428
|
+
end
|
429
|
+
spawn_model 'Foo::User' do
|
430
|
+
attributes :fullname, :email
|
431
|
+
method_for :create, 'PUT'
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
context 'for top-level class' do
|
436
|
+
it 'uses the custom method (PUT) instead of default method (POST)' do
|
437
|
+
user = Foo::User.new(:fullname => 'Tobias Fünke')
|
438
|
+
user.should be_new
|
439
|
+
user.save.should be_truthy
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
context 'for children class' do
|
444
|
+
before do
|
445
|
+
class User < Foo::User; end
|
446
|
+
@spawned_models << :User
|
447
|
+
end
|
448
|
+
|
449
|
+
it 'uses the custom method (PUT) instead of default method (POST)' do
|
450
|
+
user = User.new(:fullname => 'Tobias Fünke')
|
451
|
+
user.should be_new
|
452
|
+
user.save.should be_truthy
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
context 'update' do
|
458
|
+
before do
|
459
|
+
Her::API.default_api.connection.adapter :test do |stub|
|
460
|
+
stub.get('/users/1') { |env| [200, {}, { :id => 1, :fullname => 'Lindsay Fünke' }.to_json] }
|
461
|
+
stub.post('/users/1') { |env| [200, {}, { :id => 1, :fullname => 'Tobias Fünke' }.to_json] }
|
462
|
+
end
|
463
|
+
|
464
|
+
spawn_model 'Foo::User' do
|
465
|
+
attributes :fullname, :email
|
466
|
+
method_for :update, :post
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
it 'uses the custom method (POST) instead of default method (PUT)' do
|
471
|
+
user = Foo::User.find(1)
|
472
|
+
user.fullname.should eq 'Lindsay Fünke'
|
473
|
+
user.fullname = 'Toby Fünke'
|
474
|
+
user.save
|
475
|
+
user.fullname.should eq 'Tobias Fünke'
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|