her 0.8.6 → 0.9.0

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 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