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
data/lib/her/version.rb
ADDED
data/spec/api_spec.rb
ADDED
@@ -0,0 +1,114 @@
|
|
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
|
+
context "when setting custom middleware" do
|
10
|
+
before do
|
11
|
+
class Foo; end;
|
12
|
+
class Bar; end;
|
13
|
+
|
14
|
+
subject.setup :url => "https://api.example.com" do |connection|
|
15
|
+
connection.use Foo
|
16
|
+
connection.use Bar
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
specify { subject.connection.builder.handlers.should == [Foo, Bar] }
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when setting custom options" do
|
24
|
+
before { subject.setup :foo => { :bar => "baz" }, :url => "https://api.example.com" }
|
25
|
+
its(:options) { should == { :foo => { :bar => "baz" }, :url => "https://api.example.com" } }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#request" do
|
30
|
+
before do
|
31
|
+
class SimpleParser < Faraday::Response::Middleware
|
32
|
+
def on_complete(env)
|
33
|
+
env[:body] = { :data => env[:body] }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "making HTTP requests" do
|
39
|
+
let(:parsed_data) { subject.request(:_method => :get, :_path => "/foo")[:parsed_data] }
|
40
|
+
before do
|
41
|
+
subject.setup :url => "https://api.example.com" do |builder|
|
42
|
+
builder.use SimpleParser
|
43
|
+
builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is."] } }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
specify { parsed_data[:data].should == "Foo, it is." }
|
48
|
+
end
|
49
|
+
|
50
|
+
context "making HTTP requests while specifying custom HTTP headers" do
|
51
|
+
let(:parsed_data) { subject.request(:_method => :get, :_path => "/foo", :_headers => { "X-Page" => 2 })[:parsed_data] }
|
52
|
+
|
53
|
+
before do
|
54
|
+
subject.setup :url => "https://api.example.com" do |builder|
|
55
|
+
builder.use SimpleParser
|
56
|
+
builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is page #{env[:request_headers]["X-Page"]}."] } }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
specify { parsed_data[:data].should == "Foo, it is page 2." }
|
61
|
+
end
|
62
|
+
|
63
|
+
context "parsing a request with the default parser" do
|
64
|
+
let(:parsed_data) { subject.request(:_method => :get, :_path => "users/1")[:parsed_data] }
|
65
|
+
before do
|
66
|
+
subject.setup :url => "https://api.example.com" do |builder|
|
67
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
68
|
+
builder.adapter :test do |stub|
|
69
|
+
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 } })] }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
specify do
|
75
|
+
parsed_data[:data].should == { :id => 1, :name => "George Michael Bluth" }
|
76
|
+
parsed_data[:errors].should == ["This is a single error"]
|
77
|
+
parsed_data[:metadata].should == { :page => 1, :per_page => 10 }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "parsing a request with a custom parser" do
|
82
|
+
let(:parsed_data) { subject.request(:_method => :get, :_path => "users/1")[:parsed_data] }
|
83
|
+
before do
|
84
|
+
class CustomParser < Faraday::Response::Middleware
|
85
|
+
def on_complete(env)
|
86
|
+
json = MultiJson.load(env[:body], :symbolize_keys => true)
|
87
|
+
errors = json.delete(:errors) || []
|
88
|
+
metadata = json.delete(:metadata) || {}
|
89
|
+
env[:body] = {
|
90
|
+
:data => json,
|
91
|
+
:errors => errors,
|
92
|
+
:metadata => metadata,
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
subject.setup :url => "https://api.example.com" do |builder|
|
98
|
+
builder.use CustomParser
|
99
|
+
builder.use Faraday::Request::UrlEncoded
|
100
|
+
builder.adapter :test do |stub|
|
101
|
+
stub.get("/users/1") { |env| [200, {}, MultiJson.dump(:id => 1, :name => "George Michael Bluth")] }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
specify do
|
107
|
+
parsed_data[:data].should == { :id => 1, :name => "George Michael Bluth" }
|
108
|
+
parsed_data[:errors].should == []
|
109
|
+
parsed_data[:metadata].should == {}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
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,305 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Her::JsonApi::Model do
|
4
|
+
before do
|
5
|
+
Her::API.setup :url => "https://api.example.com" do |connection|
|
6
|
+
connection.use Her::Middleware::JsonApiParser
|
7
|
+
connection.adapter :test do |stub|
|
8
|
+
stub.get("/users/1") do |env|
|
9
|
+
[
|
10
|
+
200,
|
11
|
+
{},
|
12
|
+
{
|
13
|
+
data: {
|
14
|
+
id: 1,
|
15
|
+
type: 'users',
|
16
|
+
attributes: {
|
17
|
+
name: "Roger Federer",
|
18
|
+
},
|
19
|
+
}
|
20
|
+
|
21
|
+
}.to_json
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
stub.get("/users") do |env|
|
26
|
+
[
|
27
|
+
200,
|
28
|
+
{},
|
29
|
+
{
|
30
|
+
data: [
|
31
|
+
{
|
32
|
+
id: 1,
|
33
|
+
type: 'users',
|
34
|
+
attributes: {
|
35
|
+
name: "Roger Federer",
|
36
|
+
},
|
37
|
+
},
|
38
|
+
{
|
39
|
+
id: 2,
|
40
|
+
type: 'users',
|
41
|
+
attributes: {
|
42
|
+
name: "Kei Nishikori",
|
43
|
+
},
|
44
|
+
}
|
45
|
+
]
|
46
|
+
}.to_json
|
47
|
+
]
|
48
|
+
end
|
49
|
+
|
50
|
+
stub.post("/users", data: {
|
51
|
+
type: 'users',
|
52
|
+
attributes: {
|
53
|
+
name: "Jeremy Lin",
|
54
|
+
},
|
55
|
+
}) do |env|
|
56
|
+
[
|
57
|
+
201,
|
58
|
+
{},
|
59
|
+
{
|
60
|
+
data: {
|
61
|
+
id: 3,
|
62
|
+
type: 'users',
|
63
|
+
attributes: {
|
64
|
+
name: 'Jeremy Lin',
|
65
|
+
},
|
66
|
+
}
|
67
|
+
|
68
|
+
}.to_json
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
stub.patch("/users/1", data: {
|
73
|
+
type: 'users',
|
74
|
+
id: 1,
|
75
|
+
attributes: {
|
76
|
+
name: "Fed GOAT",
|
77
|
+
},
|
78
|
+
}) do |env|
|
79
|
+
[
|
80
|
+
200,
|
81
|
+
{},
|
82
|
+
{
|
83
|
+
data: {
|
84
|
+
id: 1,
|
85
|
+
type: 'users',
|
86
|
+
attributes: {
|
87
|
+
name: 'Fed GOAT',
|
88
|
+
},
|
89
|
+
}
|
90
|
+
|
91
|
+
}.to_json
|
92
|
+
]
|
93
|
+
end
|
94
|
+
|
95
|
+
stub.delete("/users/1") { |env|
|
96
|
+
[ 204, {}, {}, ]
|
97
|
+
}
|
98
|
+
|
99
|
+
stub.get("/players") do |env|
|
100
|
+
[
|
101
|
+
200,
|
102
|
+
{},
|
103
|
+
{
|
104
|
+
data: [
|
105
|
+
{
|
106
|
+
id: 1,
|
107
|
+
type: 'players',
|
108
|
+
attributes: { name: "Roger Federer", },
|
109
|
+
relationships: {
|
110
|
+
sponsors: {
|
111
|
+
data: [
|
112
|
+
{
|
113
|
+
type: 'sponsors',
|
114
|
+
id: 1,
|
115
|
+
},
|
116
|
+
{
|
117
|
+
type: 'sponsors',
|
118
|
+
id: 2,
|
119
|
+
}
|
120
|
+
]
|
121
|
+
},
|
122
|
+
racquet: {
|
123
|
+
data: {
|
124
|
+
type: 'racquets',
|
125
|
+
id: 1,
|
126
|
+
}
|
127
|
+
}
|
128
|
+
}
|
129
|
+
},
|
130
|
+
{
|
131
|
+
id: 2,
|
132
|
+
type: 'players',
|
133
|
+
attributes: { name: "Kei Nishikori", },
|
134
|
+
relationships: {
|
135
|
+
sponsors: {
|
136
|
+
data: [
|
137
|
+
{
|
138
|
+
type: 'sponsors',
|
139
|
+
id: 2,
|
140
|
+
},
|
141
|
+
{
|
142
|
+
type: 'sponsors',
|
143
|
+
id: 3,
|
144
|
+
}
|
145
|
+
]
|
146
|
+
},
|
147
|
+
racquet: {
|
148
|
+
data: {
|
149
|
+
type: 'racquets',
|
150
|
+
id: 2,
|
151
|
+
}
|
152
|
+
}
|
153
|
+
}
|
154
|
+
},
|
155
|
+
{
|
156
|
+
id: 3,
|
157
|
+
type: 'players',
|
158
|
+
attributes: { name: 'Hubert Huang', racquet_id: 1 },
|
159
|
+
relationships: {}
|
160
|
+
},
|
161
|
+
],
|
162
|
+
included: [
|
163
|
+
{
|
164
|
+
type: 'sponsors',
|
165
|
+
id: 1,
|
166
|
+
attributes: {
|
167
|
+
company: 'Nike',
|
168
|
+
}
|
169
|
+
},
|
170
|
+
{
|
171
|
+
type: 'sponsors',
|
172
|
+
id: 2,
|
173
|
+
attributes: {
|
174
|
+
company: 'Wilson',
|
175
|
+
},
|
176
|
+
},
|
177
|
+
{
|
178
|
+
type: 'sponsors',
|
179
|
+
id: 3,
|
180
|
+
attributes: {
|
181
|
+
company: 'Uniqlo',
|
182
|
+
},
|
183
|
+
},
|
184
|
+
{
|
185
|
+
type: 'racquets',
|
186
|
+
id: 1,
|
187
|
+
attributes: {
|
188
|
+
name: 'Wilson Pro Staff',
|
189
|
+
},
|
190
|
+
},
|
191
|
+
{
|
192
|
+
type: 'racquets',
|
193
|
+
id: 2,
|
194
|
+
attributes: {
|
195
|
+
name: 'Wilson Steam',
|
196
|
+
}
|
197
|
+
},
|
198
|
+
]
|
199
|
+
}.to_json
|
200
|
+
]
|
201
|
+
end
|
202
|
+
|
203
|
+
stub.get("/players/3/sponsors") do |env|
|
204
|
+
[
|
205
|
+
200,
|
206
|
+
{},
|
207
|
+
{ data: [] }.to_json
|
208
|
+
]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
spawn_model("Foo::User", Her::JsonApi::Model)
|
214
|
+
end
|
215
|
+
|
216
|
+
context 'simple jsonapi document' do
|
217
|
+
it 'allows configuration of type' do
|
218
|
+
spawn_model("Foo::Bar", Her::JsonApi::Model) do
|
219
|
+
type :foobars
|
220
|
+
end
|
221
|
+
|
222
|
+
expect(Foo::Bar.instance_variable_get('@type')).to eql('foobars')
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'finds models by id' do
|
226
|
+
user = Foo::User.find(1)
|
227
|
+
expect(user.attributes).to eql(
|
228
|
+
'id' => 1,
|
229
|
+
'name' => 'Roger Federer',
|
230
|
+
)
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'finds a collection of models' do
|
234
|
+
users = Foo::User.all
|
235
|
+
expect(users.map(&:attributes)).to match_array([
|
236
|
+
{
|
237
|
+
'id' => 1,
|
238
|
+
'name' => 'Roger Federer',
|
239
|
+
},
|
240
|
+
{
|
241
|
+
'id' => 2,
|
242
|
+
'name' => 'Kei Nishikori',
|
243
|
+
}
|
244
|
+
])
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'creates a Foo::User' do
|
248
|
+
user = Foo::User.new(name: 'Jeremy Lin')
|
249
|
+
user.save
|
250
|
+
expect(user.attributes).to eql(
|
251
|
+
'id' => 3,
|
252
|
+
'name' => 'Jeremy Lin',
|
253
|
+
)
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'updates a Foo::User' do
|
257
|
+
user = Foo::User.find(1)
|
258
|
+
user.name = 'Fed GOAT'
|
259
|
+
user.save
|
260
|
+
expect(user.attributes).to eql(
|
261
|
+
'id' => 1,
|
262
|
+
'name' => 'Fed GOAT',
|
263
|
+
)
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'destroys a Foo::User' do
|
267
|
+
user = Foo::User.find(1)
|
268
|
+
expect(user.destroy).to be_destroyed
|
269
|
+
end
|
270
|
+
|
271
|
+
context 'undefined methods' do
|
272
|
+
it 'removes methods that are not compatible with json api' do
|
273
|
+
[:parse_root_in_json, :include_root_in_json, :root_element, :primary_key].each do |method|
|
274
|
+
expect { Foo::User.new.send(method, :foo) }.to raise_error NoMethodError, "Her::JsonApi::Model does not support the #{method} configuration option"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
context 'compound document' do
|
281
|
+
before do
|
282
|
+
spawn_model("Foo::Sponsor", Her::JsonApi::Model)
|
283
|
+
spawn_model("Foo::Racquet", Her::JsonApi::Model)
|
284
|
+
spawn_model("Foo::Player", Her::JsonApi::Model) do
|
285
|
+
has_many :sponsors
|
286
|
+
has_one :racquet
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'parses included documents into object if relationship specifieds a resource linkage' do
|
291
|
+
players = Foo::Player.all.to_a
|
292
|
+
fed = players.detect { |p| p.name == 'Roger Federer' }
|
293
|
+
expect(fed.sponsors.map(&:company)).to match_array ['Nike', 'Wilson']
|
294
|
+
expect(fed.racquet.name).to eq 'Wilson Pro Staff'
|
295
|
+
|
296
|
+
kei = players.detect { |p| p.name == 'Kei Nishikori' }
|
297
|
+
expect(kei.sponsors.map(&:company)).to match_array ['Uniqlo', 'Wilson']
|
298
|
+
expect(kei.racquet.name).to eq 'Wilson Steam'
|
299
|
+
|
300
|
+
hubert = players.detect { |p| p.name == 'Hubert Huang' }
|
301
|
+
expect(hubert.sponsors.map(&:company)).to eq []
|
302
|
+
expect(hubert.racquet.name).to be_nil
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|