her 0.8.6 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bd29346168938bbc64f498b73226188938c7cdb5
4
- data.tar.gz: 86554e2fa519f825aedb4fa63a3c82e50b4a8731
3
+ metadata.gz: 458601a1de64f520013856ef805b17e8293bfb9d
4
+ data.tar.gz: 1e965c550664039d6b6f649a9b10a7821d90f717
5
5
  SHA512:
6
- metadata.gz: ffd1a26120cb29cab5d879515b8c587c65f5729aaa61735998dd5e94a6f5af717399e8c4f03dcb2098fbc4385645312335b477f14e3559e90afd348a850cf087
7
- data.tar.gz: f2a023b8d8c0330988c104664026c26ffa5e6e34554724b4379f7d2edddb4aec306de9b6fe032b1c75d85b0c0238e170db20534d8e54bc9236b985b937c3754a
6
+ metadata.gz: e7d147d58050f847942df92440c9e71013c1fc54fbb56d0a6e5dd9d6167a1ffc4e30c329524f1bac07a378d06991198cb1bf44215067ab911f2d0ff1ee5b95f7
7
+ data.tar.gz: 1c3ab969c7fbe8f90f730945cd9392f9e2d3df6b3df577291f86f97aa7405758f1086a7708e2586760d674a362703c465bd66516f828d1c1a389fb7c304f4a16
data/.travis.yml CHANGED
@@ -3,6 +3,7 @@ language: ruby
3
3
  sudo: false
4
4
 
5
5
  rvm:
6
+ - 2.4.1
6
7
  - 2.3.1
7
8
  - 2.2.2
8
9
  - 2.1.6
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # Maintenance Update 29th Sept 2016
2
+
2
3
  Hi folks, [@edtjones](https://github.com/edtjones) here. Rémi has handed me the keys to Her and [@foxpaul](https://github.com/foxpaul) and I will be trying to do the library justice with the help of the community. There's loads to do; we'll get in touch with everyone who's raised a PR as soon as possible and figure out a plan of action.
3
4
 
4
5
  # Rails 5 support
@@ -15,6 +16,7 @@ If you need Rails 5 support, version 0.8.2 is for you!
15
16
  <a href="https://codeclimate.com/github/remiprev/her"><img src="http://img.shields.io/codeclimate/github/remiprev/her.svg" /></a>
16
17
  <a href='https://gemnasium.com/remiprev/her'><img src="http://img.shields.io/gemnasium/remiprev/her.svg" /></a>
17
18
  <a href="https://travis-ci.org/remiprev/her"><img src="http://img.shields.io/travis/remiprev/her/master.svg" /></a>
19
+ <a href="https://gitter.im/her-orm/Lobby"><img src="https://badges.gitter.im/her-orm/Lobby.png" alt="Gitter chat" title="" data-pin-nopin="true"></a>
18
20
  </p>
19
21
 
20
22
  ---
@@ -182,7 +184,7 @@ Her::API.setup url: "https://api.example.com" do |c|
182
184
  # Request
183
185
  c.use Faraday::Request::BasicAuthentication, 'myusername', 'mypassword'
184
186
  c.use Faraday::Request::UrlEncoded
185
-
187
+
186
188
  # Response
187
189
  c.use Her::Middleware::DefaultParseJSON
188
190
 
@@ -611,7 +613,7 @@ users = Users.all
611
613
  #### JSON API support
612
614
 
613
615
  To consume a JSON API 1.0 compliant service, it must return data in accordance with the [JSON API spec](http://jsonapi.org/). The general format
614
- of the data is as follows:
616
+ of the data is as follows:
615
617
 
616
618
  ```json
617
619
  { "data": {
data/lib/her/api.rb CHANGED
@@ -6,7 +6,7 @@ module Her
6
6
  attr_reader :connection, :options
7
7
 
8
8
  # Constants
9
- FARADAY_OPTIONS = [:request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class, :timeout, :open_timeout].freeze
9
+ FARADAY_OPTIONS = [:request, :proxy, :ssl, :builder, :url, :parallel_manager, :params, :headers, :builder_class].freeze
10
10
 
11
11
  # Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
12
12
  def self.setup(opts={}, &block)
@@ -25,7 +25,7 @@ module Her
25
25
  # @private
26
26
  def on_complete(env)
27
27
  env[:body] = case env[:status]
28
- when 204
28
+ when 204, 304
29
29
  parse('{}')
30
30
  else
31
31
  parse(env[:body])
@@ -49,6 +49,7 @@ module Her
49
49
 
50
50
  return @cached_result unless @params.any? || @cached_result.nil?
51
51
  return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
52
+ return @opts[:default].try(:dup) if @parent.new?
52
53
 
53
54
  path = build_association_path lambda { "#{@parent.request_path(@params)}#{@opts[:path]}" }
54
55
  @klass.get(path, @params).tap do |result|
@@ -65,6 +66,13 @@ module Her
65
66
  end
66
67
  end
67
68
 
69
+ # @private
70
+ def reset
71
+ @params = {}
72
+ @cached_result = nil
73
+ @parent.attributes.delete(@name)
74
+ end
75
+
68
76
  # Add query parameters to the HTTP request performed to fetch the data
69
77
  #
70
78
  # @example
@@ -97,6 +105,29 @@ module Her
97
105
  @klass.get_resource(path, @params)
98
106
  end
99
107
 
108
+ # Refetches the association and puts the proxy back in its initial state,
109
+ # which is unloaded. Cached associations are cleared.
110
+ #
111
+ # @example
112
+ # class User
113
+ # include Her::Model
114
+ # has_many :comments
115
+ # end
116
+ #
117
+ # class Comment
118
+ # include Her::Model
119
+ # end
120
+ #
121
+ # user = User.find(1)
122
+ # user.comments = [#<Comment(comments/2) id=2 body="Hello!">]
123
+ # user.comments.first.id = "Oops"
124
+ # user.comments.reload # => [#<Comment(comments/2) id=2 body="Hello!">]
125
+ # # Fetched again via GET "/users/1/comments"
126
+ def reload
127
+ reset
128
+ fetch
129
+ end
130
+
100
131
  end
101
132
  end
102
133
  end
@@ -15,7 +15,7 @@ module Her
15
15
  end
16
16
 
17
17
  install_proxy_methods :association,
18
- :build, :create, :where, :find, :all, :assign_nested_attributes
18
+ :build, :create, :where, :find, :all, :assign_nested_attributes, :reload
19
19
 
20
20
  # @private
21
21
  def initialize(association)
@@ -25,6 +25,8 @@ module Her
25
25
 
26
26
  attributes = self.class.default_scope.apply_to(attributes)
27
27
  assign_attributes(attributes)
28
+
29
+ yield self if block_given?
28
30
  run_callbacks :initialize
29
31
  end
30
32
 
data/lib/her/model/orm.rb CHANGED
@@ -39,8 +39,8 @@ module Her
39
39
  callback = new? ? :create : :update
40
40
  method = self.class.method_for(callback)
41
41
 
42
- run_callbacks callback do
43
- run_callbacks :save do
42
+ run_callbacks :save do
43
+ run_callbacks callback do
44
44
  params = to_params
45
45
  self.class.request(to_params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
46
46
  assign_attributes(self.class.parse(parsed_data[:data])) if parsed_data[:data].any?
@@ -49,7 +49,7 @@ module Her
49
49
 
50
50
  return false if !response.success? || @response_errors.any?
51
51
  if self.changed_attributes.present?
52
- @previously_changed = self.changed_attributes.clone
52
+ @previously_changed = self.changes.clone
53
53
  self.changed_attributes.clear
54
54
  end
55
55
  end
@@ -86,6 +86,75 @@ module Her
86
86
  self
87
87
  end
88
88
 
89
+ # Refetches the resource
90
+ #
91
+ # This method finds the resource by its primary key (which could be
92
+ # assigned manually) and modifies the object in-place.
93
+ #
94
+ # @example
95
+ # user = User.find(1)
96
+ # # => #<User(users/1) id=1 name="Tobias Fünke">
97
+ # user.name = "Oops"
98
+ # user.reload # Fetched again via GET "/users/1"
99
+ # # => #<User(users/1) id=1 name="Tobias Fünke">
100
+ def reload(options = nil)
101
+ fresh_object = self.class.find(id)
102
+ assign_attributes(fresh_object.attributes)
103
+ self
104
+ end
105
+
106
+ # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
107
+ # if the predicate returns +true+ the attribute will become +false+. This
108
+ # method toggles directly the underlying value without calling any setter.
109
+ # Returns +self+.
110
+ #
111
+ # @example
112
+ # user = User.first
113
+ # user.admin? # => false
114
+ # user.toggle(:admin)
115
+ # user.admin? # => true
116
+ def toggle(attribute)
117
+ attributes[attribute] = !public_send("#{attribute}?")
118
+ self
119
+ end
120
+
121
+ # Wrapper around #toggle that saves the resource. Saving is subjected to
122
+ # validation checks. Returns +true+ if the record could be saved.
123
+ def toggle!(attribute)
124
+ toggle(attribute) && save
125
+ end
126
+
127
+ # Initializes +attribute+ to zero if +nil+ and adds the value passed as
128
+ # +by+ (default is 1). The increment is performed directly on the
129
+ # underlying attribute, no setter is invoked. Only makes sense for
130
+ # number-based attributes. Returns +self+.
131
+ def increment(attribute, by = 1)
132
+ attributes[attribute] ||= 0
133
+ attributes[attribute] += by
134
+ self
135
+ end
136
+
137
+ # Wrapper around #increment that saves the resource. Saving is subjected
138
+ # to validation checks. Returns +self+.
139
+ def increment!(attribute, by = 1)
140
+ increment(attribute, by) && save
141
+ self
142
+ end
143
+
144
+ # Initializes +attribute+ to zero if +nil+ and substracts the value passed as
145
+ # +by+ (default is 1). The decrement is performed directly on the
146
+ # underlying attribute, no setter is invoked. Only makes sense for
147
+ # number-based attributes. Returns +self+.
148
+ def decrement(attribute, by = 1)
149
+ increment(attribute, -by)
150
+ end
151
+
152
+ # Wrapper around #decrement that saves the resource. Saving is subjected
153
+ # to validation checks. Returns +self+.
154
+ def decrement!(attribute, by = 1)
155
+ increment!(attribute, -by)
156
+ end
157
+
89
158
  module ClassMethods
90
159
  # Create a new chainable scope
91
160
  #
@@ -134,7 +203,8 @@ module Her
134
203
  end
135
204
 
136
205
  # Delegate the following methods to `scoped`
137
- [:all, :where, :create, :build, :find, :first_or_create, :first_or_initialize].each do |method|
206
+ [:all, :where, :create, :build, :find, :find_by, :find_or_create_by,
207
+ :find_or_initialize_by, :first_or_create, :first_or_initialize].each do |method|
138
208
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
139
209
  def #{method}(*params)
140
210
  scoped.send(#{method.to_sym.inspect}, *params)
@@ -52,20 +52,16 @@ module Her
52
52
  end
53
53
  end
54
54
 
55
-
56
55
  # @private
57
- # TODO: Handle has_one
58
56
  def embeded_params(attributes)
59
- associations[:has_many].select { |a| attributes.include?(a[:data_key])}.compact.inject({}) do |hash, association|
60
- params = attributes[association[:data_key]].map(&:to_params)
61
- next hash if params.empty?
62
- if association[:class_name].constantize.include_root_in_json?
63
- root = association[:class_name].constantize.root_element
64
- hash[association[:data_key]] = params.map { |n| n[root] }
65
- else
66
- hash[association[:data_key]] = params
67
- end
68
- hash
57
+ associations.values.flatten.each_with_object({}) do |definition, hash|
58
+ value = case association = attributes[definition[:name]]
59
+ when Her::Collection, Array
60
+ association.map { |a| a.to_params }.reject(&:empty?)
61
+ when Her::Model
62
+ association.to_params
63
+ end
64
+ hash[definition[:data_key]] = value if value.present?
69
65
  end
70
66
  end
71
67
 
@@ -65,7 +65,7 @@ module Her
65
65
  # @private
66
66
  def fetch
67
67
  @_fetch ||= begin
68
- path = @parent.build_request_path(@params)
68
+ path = @parent.build_request_path(@parent.collection_path, @params)
69
69
  method = @parent.method_for(:find)
70
70
  @parent.request(@params.merge(:_method => method, :_path => path)) do |parsed_data, response|
71
71
  @parent.new_collection(parsed_data)
@@ -109,6 +109,50 @@ module Her
109
109
  ids.length > 1 || ids.first.kind_of?(Array) ? results : results.first
110
110
  end
111
111
 
112
+ # Fetch first resource with the given attributes.
113
+ #
114
+ # If no resource is found, returns <tt>nil</tt>.
115
+ #
116
+ # @example
117
+ # @user = User.find_by(name: "Tobias", age: 42)
118
+ # # Called via GET "/users?name=Tobias&age=42"
119
+ def find_by(params)
120
+ where(params).first
121
+ end
122
+
123
+ # Fetch first resource with the given attributes, or create a resource
124
+ # with the attributes if one is not found.
125
+ #
126
+ # @example
127
+ # @user = User.find_or_create_by(email: "remi@example.com")
128
+ #
129
+ # # Returns the first item in the collection if present:
130
+ # # Called via GET "/users?email=remi@example.com"
131
+ #
132
+ # # If collection is empty:
133
+ # # POST /users with `email=remi@example.com`
134
+ # @user.email # => "remi@example.com"
135
+ # @user.new? # => false
136
+ def find_or_create_by(attributes)
137
+ find_by(attributes) || create(attributes)
138
+ end
139
+
140
+ # Fetch first resource with the given attributes, or initialize a resource
141
+ # with the attributes if one is not found.
142
+ #
143
+ # @example
144
+ # @user = User.find_or_initialize_by(email: "remi@example.com")
145
+ #
146
+ # # Returns the first item in the collection if present:
147
+ # # Called via GET "/users?email=remi@example.com"
148
+ #
149
+ # # If collection is empty:
150
+ # @user.email # => "remi@example.com"
151
+ # @user.new? # => true
152
+ def find_or_initialize_by(attributes)
153
+ find_by(attributes) || build(attributes)
154
+ end
155
+
112
156
  # Create a resource and return it
113
157
  #
114
158
  # @example
data/lib/her/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Her
2
- VERSION = "0.8.6"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -22,6 +22,26 @@ describe Her::Middleware::JsonApiParser do
22
22
  end
23
23
  end
24
24
 
25
+ context "with status code 204" do
26
+ it "returns an empty body" do
27
+ env = { status: 204 }
28
+ subject.on_complete(env)
29
+ env[:body].tap do |json|
30
+ expect(json[:data]).to eq({})
31
+ end
32
+ end
33
+ end
34
+
35
+ context 'with status code 304' do
36
+ it 'returns an empty body' do
37
+ env = { :status => 304 }
38
+ subject.on_complete(env)
39
+ env[:body].tap do |json|
40
+ expect(json[:data]).to eq({})
41
+ end
42
+ end
43
+ end
44
+
25
45
  # context "with invalid JSON body" do
26
46
  # let(:body) { '"foo"' }
27
47
  # it 'ensures that invalid JSON throws an exception' do
@@ -153,14 +153,16 @@ describe Her::Model::Associations do
153
153
 
154
154
  spawn_model "Foo::User" do
155
155
  has_many :comments, class_name: "Foo::Comment"
156
- has_one :role
157
- belongs_to :organization
156
+ has_one :role, class_name: "Foo::Role"
157
+ belongs_to :organization, class_name: "Foo::Organization"
158
158
  has_many :posts, inverse_of: :admin
159
159
  end
160
+
160
161
  spawn_model "Foo::Comment" do
161
162
  belongs_to :user
162
163
  parse_root_in_json true
163
164
  end
165
+
164
166
  spawn_model "Foo::Post" do
165
167
  belongs_to :admin, class_name: "Foo::User"
166
168
  end
@@ -180,6 +182,7 @@ describe Her::Model::Associations do
180
182
  let(:user_with_included_data_after_save_existing) { Foo::User.save_existing(5, name: "Clancy Brown") }
181
183
  let(:user_with_included_data_after_destroy) { Foo::User.new(id: 5).destroy }
182
184
  let(:comment_without_included_parent_data) { Foo::Comment.new(id: 7, user_id: 1) }
185
+ let(:new_user) { Foo::User.new }
183
186
 
184
187
  it "maps an array of included data through has_many" do
185
188
  expect(@user_with_included_data.comments.first).to be_a(Foo::Comment)
@@ -204,6 +207,12 @@ describe Her::Model::Associations do
204
207
  expect(@user_with_included_data.posts.first.admin.object_id).to eq(@user_with_included_data.object_id)
205
208
  end
206
209
 
210
+ it "doesn't attempt to fetch association data for a new resource" do
211
+ expect(new_user.comments).to eq([])
212
+ expect(new_user.role).to be_nil
213
+ expect(new_user.organization).to be_nil
214
+ end
215
+
207
216
  it "fetches data that was not included through has_many" do
208
217
  expect(@user_without_included_data.comments.first).to be_a(Foo::Comment)
209
218
  expect(@user_without_included_data.comments.length).to eq(2)
@@ -223,6 +232,10 @@ describe Her::Model::Associations do
223
232
  expect(@user_without_included_data.comments.first.object_id).not_to eq(@user_without_included_data.comments.where(foo_id: 1).first.object_id)
224
233
  end
225
234
 
235
+ it "fetches data again after being reloaded" do
236
+ expect { @user_without_included_data.comments.reload }.to change { @user_without_included_data.comments.first.object_id }
237
+ end
238
+
226
239
  it "maps an array of included data through has_one" do
227
240
  expect(@user_with_included_data.role).to be_a(Foo::Role)
228
241
  expect(@user_with_included_data.role.object_id).to eq(@user_with_included_data.role.object_id)
@@ -292,6 +305,18 @@ describe Her::Model::Associations do
292
305
  expect(params[:comments].length).to eq(2)
293
306
  end
294
307
 
308
+ it "includes has_one relationship in params by default" do
309
+ params = @user_with_included_data.to_params
310
+ expect(params[:role]).to be_kind_of(Hash)
311
+ expect(params[:role]).not_to be_empty
312
+ end
313
+
314
+ it "includes belongs_to relationship in params by default" do
315
+ params = @user_with_included_data.to_params
316
+ expect(params[:organization]).to be_kind_of(Hash)
317
+ expect(params[:organization]).not_to be_empty
318
+ end
319
+
295
320
  [:create, :save_existing, :destroy].each do |type|
296
321
  context "after #{type}" do
297
322
  let(:subject) { send("user_with_included_data_after_#{type}") }
@@ -326,15 +351,24 @@ describe Her::Model::Associations do
326
351
  stub.get("/organizations/1") { [200, {}, { organization: { id: 1, name: "Bluth Company Foo" } }.to_json] }
327
352
  end
328
353
  end
354
+
329
355
  spawn_model "Foo::User" do
330
356
  parse_root_in_json true, format: :active_model_serializers
331
357
  has_many :comments, class_name: "Foo::Comment"
332
- belongs_to :organization
358
+ has_one :role, class_name: "Foo::Role"
359
+ belongs_to :organization, class_name: "Foo::Organization"
333
360
  end
361
+
362
+ spawn_model "Foo::Role" do
363
+ belongs_to :user
364
+ parse_root_in_json true, format: :active_model_serializers
365
+ end
366
+
334
367
  spawn_model "Foo::Comment" do
335
368
  belongs_to :user
336
369
  parse_root_in_json true, format: :active_model_serializers
337
370
  end
371
+
338
372
  spawn_model "Foo::Organization" do
339
373
  parse_root_in_json true, format: :active_model_serializers
340
374
  end
@@ -392,6 +426,18 @@ describe Her::Model::Associations do
392
426
  expect(params[:comments]).to be_kind_of(Array)
393
427
  expect(params[:comments].length).to eq(2)
394
428
  end
429
+
430
+ it "includes has_one relationships in params by default" do
431
+ params = @user_with_included_data.to_params
432
+ expect(params[:role]).to be_kind_of(Hash)
433
+ expect(params[:role]).not_to be_empty
434
+ end
435
+
436
+ it "includes belongs_to relationship in params by default" do
437
+ params = @user_with_included_data.to_params
438
+ expect(params[:organization]).to be_kind_of(Hash)
439
+ expect(params[:organization]).not_to be_empty
440
+ end
395
441
  end
396
442
 
397
443
  context "handling associations with details" do
@@ -11,6 +11,14 @@ describe Her::Model::Attributes do
11
11
  expect(@new_user.fullname).to eq("Tobias Fünke")
12
12
  end
13
13
 
14
+ it "handles new resource with block" do
15
+ @new_user = Foo::User.new do |user|
16
+ user.fullname = "Tobias Fünke"
17
+ end
18
+ expect(@new_user.new?).to be_truthy
19
+ expect(@new_user.fullname).to eq("Tobias Fünke")
20
+ end
21
+
14
22
  it "accepts new resource with strings as hash keys" do
15
23
  @new_user = Foo::User.new("fullname" => "Tobias Fünke")
16
24
  expect(@new_user.fullname).to eq("Tobias Fünke")
@@ -49,7 +49,7 @@ describe "Her::Model and ActiveModel::Dirty" do
49
49
  it "tracks previous changes" do
50
50
  user.fullname = "Tobias Fünke"
51
51
  user.save
52
- expect(user.previous_changes).to eq("fullname" => "Lindsay Fünke")
52
+ expect(user.previous_changes).to eq("fullname" => ["Lindsay Fünke", "Tobias Fünke"])
53
53
  end
54
54
 
55
55
  it "tracks dirty attribute for mass assign for dynamic created attributes" do
@@ -107,12 +107,12 @@ describe Her::Model::ORM do
107
107
 
108
108
  it "handles metadata on a destroyed resource" do
109
109
  @user = User.destroy_existing(1)
110
- @user.metadata[:foo].should == "bar"
110
+ expect(@user.metadata[:foo]).to eq("bar")
111
111
  end
112
112
 
113
113
  it "handles error data on a destroyed resource" do
114
114
  @user = User.destroy_existing(1)
115
- @user.response_errors.should == ["Yes", "Sir"]
115
+ expect(@user.response_errors).to eq(%w(Yes Sir))
116
116
  end
117
117
  end
118
118
 
@@ -212,6 +212,8 @@ describe Her::Model::ORM do
212
212
  stub.get("/users?age=42&foo=bar") { [200, {}, [{ id: 3, age: 42 }].to_json] }
213
213
  stub.get("/users?age=42") { [200, {}, [{ id: 1, age: 42 }].to_json] }
214
214
  stub.get("/users?age=40") { [200, {}, [{ id: 1, age: 40 }].to_json] }
215
+ stub.get("/users?name=baz") { [200, {}, [].to_json] }
216
+ stub.post("/users") { [200, {}, { id: 5, name: "baz" }.to_json] }
215
217
  end
216
218
  end
217
219
 
@@ -264,6 +266,24 @@ describe Her::Model::ORM do
264
266
  expect(@users[1].id).to eq(2)
265
267
  end
266
268
 
269
+ it "handles finding by attributes" do
270
+ @user = User.find_by(age: 42)
271
+ expect(@user).to be_a(User)
272
+ expect(@user.id).to eq(1)
273
+ end
274
+
275
+ it "handles find or create by attributes" do
276
+ @user = User.find_or_create_by(name: "baz")
277
+ expect(@user).to be_a(User)
278
+ expect(@user.id).to eq(5)
279
+ end
280
+
281
+ it "handles find or initialize by attributes" do
282
+ @user = User.find_or_initialize_by(name: "baz")
283
+ expect(@user).to be_a(User)
284
+ expect(@user).to_not be_persisted
285
+ end
286
+
267
287
  it "handles finding with other parameters" do
268
288
  @users = User.where(age: 42, foo: "bar").all
269
289
  expect(@users).to be_kind_of(Array)
@@ -275,6 +295,14 @@ describe Her::Model::ORM do
275
295
  expect(@users.where(age: 42)).to be_all { |u| u.age == 42 }
276
296
  expect(@users.where(age: 40)).to be_all { |u| u.age == 40 }
277
297
  end
298
+
299
+ it "handles reloading a resource" do
300
+ @user = User.find(1)
301
+ @user.age = "Oops"
302
+ @user.reload
303
+ expect(@user.age).to eq 42
304
+ expect(@user).to be_persisted
305
+ end
278
306
  end
279
307
 
280
308
  context "building resources" do
@@ -371,12 +399,15 @@ describe Her::Model::ORM do
371
399
  builder.use Her::Middleware::FirstLevelParseJSON
372
400
  builder.use Faraday::Request::UrlEncoded
373
401
  builder.adapter :test do |stub|
374
- stub.get("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke" }.to_json] }
375
- stub.put("/users/1") { [200, {}, { id: 1, fullname: "Lindsay Fünke" }.to_json] }
402
+ stub.get("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke", admin: false }.to_json] }
403
+ stub.put("/users/1") { [200, {}, { id: 1, fullname: "Lindsay Fünke", admin: true }.to_json] }
404
+ stub.get("/pages/1") { [200, {}, { id: 1, views: 1, unique_visitors: 4 }.to_json] }
405
+ stub.put("/pages/1") { [200, {}, { id: 1, views: 2, unique_visitors: 3 }.to_json] }
376
406
  end
377
407
  end
378
408
 
379
409
  spawn_model "Foo::User"
410
+ spawn_model "Foo::Page"
380
411
  end
381
412
 
382
413
  it "handle resource data update without saving it" do
@@ -397,6 +428,58 @@ describe Her::Model::ORM do
397
428
  @user.save
398
429
  expect(@user.fullname).to eq("Lindsay Fünke")
399
430
  end
431
+
432
+ it "handles resource update through #toggle without saving it" do
433
+ @user = Foo::User.find(1)
434
+ expect(@user.admin).to be_falsey
435
+ expect(@user).to_not receive(:save)
436
+ @user.toggle(:admin)
437
+ expect(@user.admin).to be_truthy
438
+ end
439
+
440
+ it "handles resource update through #toggle!" do
441
+ @user = Foo::User.find(1)
442
+ expect(@user.admin).to be_falsey
443
+ expect(@user).to receive(:save).and_return(true)
444
+ @user.toggle!(:admin)
445
+ expect(@user.admin).to be_truthy
446
+ end
447
+
448
+ it "handles resource update through #increment without saving it" do
449
+ page = Foo::Page.find(1)
450
+ expect(page.views).to be 1
451
+ expect(page).to_not receive(:save)
452
+ page.increment(:views)
453
+ expect(page.views).to be 2
454
+ page.increment(:views, 2)
455
+ expect(page.views).to be 4
456
+ end
457
+
458
+ it "handles resource update through #increment!" do
459
+ page = Foo::Page.find(1)
460
+ expect(page.views).to be 1
461
+ expect(page).to receive(:save).and_return(true)
462
+ page.increment!(:views)
463
+ expect(page.views).to be 2
464
+ end
465
+
466
+ it "handles resource update through #decrement without saving it" do
467
+ page = Foo::Page.find(1)
468
+ expect(page.unique_visitors).to be 4
469
+ expect(page).to_not receive(:save)
470
+ page.decrement(:unique_visitors)
471
+ expect(page.unique_visitors).to be 3
472
+ page.decrement(:unique_visitors, 2)
473
+ expect(page.unique_visitors).to be 1
474
+ end
475
+
476
+ it "handles resource update through #decrement!" do
477
+ page = Foo::Page.find(1)
478
+ expect(page.unique_visitors).to be 4
479
+ expect(page).to receive(:save).and_return(true)
480
+ page.decrement!(:unique_visitors)
481
+ expect(page.unique_visitors).to be 3
482
+ end
400
483
  end
401
484
 
402
485
  context "deleting resources" do
@@ -530,4 +613,51 @@ describe Her::Model::ORM do
530
613
  end
531
614
  end
532
615
  end
616
+
617
+ context "registering callbacks" do
618
+ before do
619
+ Her::API.setup url: "https://api.example.com" do |builder|
620
+ builder.use Her::Middleware::FirstLevelParseJSON
621
+ builder.use Faraday::Request::UrlEncoded
622
+ builder.adapter :test do |stub|
623
+ stub.get("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke" }.to_json] }
624
+ stub.put("/users/1") { [200, {}, { id: 1, fullname: "Tobias Fünke" }.to_json] }
625
+ stub.post("/users") { [200, {}, { id: 2, fullname: "Lindsay Fünke" }.to_json] }
626
+ end
627
+ end
628
+
629
+ spawn_model "User" do
630
+ before_save :before_save_callback
631
+ before_create :before_create_callback
632
+ before_update :before_update_callback
633
+ after_update :after_update_callback
634
+ after_create :after_create_callback
635
+ after_save :after_save_callback
636
+ def before_save_callback; end
637
+ def before_create_callback; end
638
+ def before_update_callback; end
639
+ def after_update_callback; end
640
+ def after_create_callback; end
641
+ def after_save_callback; end
642
+ end
643
+ end
644
+
645
+ it "runs create callbacks in the correct order" do
646
+ @user = User.new(fullname: "Tobias Fünke")
647
+ expect(@user).to receive(:before_save_callback).ordered
648
+ expect(@user).to receive(:before_create_callback).ordered
649
+ expect(@user).to receive(:after_create_callback).ordered
650
+ expect(@user).to receive(:after_save_callback).ordered
651
+ @user.save
652
+ end
653
+
654
+ it "runs update callbacks in the correct order" do
655
+ @user = User.find(1)
656
+ expect(@user).to receive(:before_save_callback).ordered
657
+ expect(@user).to receive(:before_update_callback).ordered
658
+ expect(@user).to receive(:after_update_callback).ordered
659
+ expect(@user).to receive(:after_save_callback).ordered
660
+ @user.save
661
+ end
662
+ end
533
663
  end
@@ -10,6 +10,7 @@ describe Her::Model::Relation do
10
10
  builder.adapter :test do |stub|
11
11
  stub.get("/users?foo=1&bar=2") { ok! [{ id: 2, fullname: "Tobias Fünke" }] }
12
12
  stub.get("/users?admin=1") { ok! [{ id: 1, fullname: "Tobias Fünke" }] }
13
+ stub.get("/users?id=3&foo=2") { ok! [{ id: 3, fullname: "Tobias Fünke" }] }
13
14
 
14
15
  stub.get("/users") do
15
16
  ok! [
@@ -41,6 +42,13 @@ describe Her::Model::Relation do
41
42
  expect(@users.size).to eql 1
42
43
  end
43
44
 
45
+ it "fetches the data by parameters including primary_key" do
46
+ expect(Foo::User).to receive(:request).once.and_call_original
47
+ @users = Foo::User.where(id: 3, foo: 2)
48
+ expect(@users).to respond_to(:length)
49
+ expect(@users.size).to eql 1
50
+ end
51
+
44
52
  it "chains multiple where statements" do
45
53
  @user = Foo::User.where(foo: 1).where(bar: 2).first
46
54
  expect(@user.id).to eq(2)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: her
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.6
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rémi Prévost
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-14 00:00:00.000000000 Z
11
+ date: 2017-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake