her 0.6.8 → 0.7

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.
@@ -2,6 +2,7 @@ module Her
2
2
  module Model
3
3
  module Associations
4
4
  class HasManyAssociation < Association
5
+
5
6
  # @private
6
7
  def self.attach(klass, name, opts)
7
8
  opts = {
@@ -19,7 +20,7 @@ module Her
19
20
  cached_name = :"@_her_association_#{name}"
20
21
 
21
22
  cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
22
- cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.new(self, #{opts.inspect}))
23
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.proxy(self, #{opts.inspect}))
23
24
  end
24
25
  RUBY
25
26
  end
@@ -2,6 +2,7 @@ module Her
2
2
  module Model
3
3
  module Associations
4
4
  class HasOneAssociation < Association
5
+
5
6
  # @private
6
7
  def self.attach(klass, name, opts)
7
8
  opts = {
@@ -18,7 +19,7 @@ module Her
18
19
  cached_name = :"@_her_association_#{name}"
19
20
 
20
21
  cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
21
- cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.new(self, #{opts.inspect}))
22
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.proxy(self, #{opts.inspect}))
22
23
  end
23
24
  RUBY
24
25
  end
@@ -33,8 +33,12 @@ module Her
33
33
  # @private
34
34
  def self.initialize_collection(klass, parsed_data={})
35
35
  collection_data = klass.extract_array(parsed_data).map do |item_data|
36
- resource = klass.new(klass.parse(item_data))
37
- resource.run_callbacks :find
36
+ if item_data.kind_of?(klass)
37
+ resource = item_data
38
+ else
39
+ resource = klass.new(klass.parse(item_data))
40
+ resource.run_callbacks :find
41
+ end
38
42
  resource
39
43
  end
40
44
  Her::Collection.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
@@ -77,14 +81,9 @@ module Her
77
81
  end
78
82
  end
79
83
 
80
- # @private
81
- def respond_to?(method, include_private = false)
82
- method.to_s.end_with?('=') || method.to_s.end_with?('?') || @attributes.include?(method) || super
83
- end
84
-
85
84
  # @private
86
85
  def respond_to_missing?(method, include_private = false)
87
- method.to_s.end_with?('=') || method.to_s.end_with?('?') || @attributes.include?(method) || @attributes.include?(method) || super
86
+ method.to_s.end_with?('=') || method.to_s.end_with?('?') || @attributes.include?(method) || super
88
87
  end
89
88
 
90
89
  # Assign new attributes to a resource
@@ -168,6 +167,7 @@ module Her
168
167
  #
169
168
  # @private
170
169
  def new_from_parsed_data(parsed_data)
170
+ parsed_data = parsed_data.with_indifferent_access
171
171
  new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
172
172
  end
173
173
 
@@ -185,20 +185,19 @@ module Her
185
185
  attributes.each do |attribute|
186
186
  attribute = attribute.to_sym
187
187
 
188
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
189
- unless instance_methods.include?(:'#{attribute}=')
190
- def #{attribute}=(value)
191
- self.send(:"#{attribute}_will_change!") if @attributes[:'#{attribute}'] != value
192
- @attributes[:'#{attribute}'] = value
193
- end
188
+ unless instance_methods.include?(:"#{attribute}=")
189
+ define_method("#{attribute}=") do |value|
190
+ @attributes[:"#{attribute}"] = nil unless @attributes.include?(:"#{attribute}")
191
+ self.send(:"#{attribute}_will_change!") if @attributes[:'#{attribute}'] != value
192
+ @attributes[:"#{attribute}"] = value
194
193
  end
194
+ end
195
195
 
196
- unless instance_methods.include?(:'#{attribute}?')
197
- def #{attribute}?
198
- @attributes.include?(:'#{attribute}') && @attributes[:'#{attribute}'].present?
199
- end
196
+ unless instance_methods.include?(:"#{attribute}?")
197
+ define_method("#{attribute}?") do
198
+ @attributes.include?(:"#{attribute}") && @attributes[:"#{attribute}"].present?
200
199
  end
201
- RUBY
200
+ end
202
201
  end
203
202
  end
204
203
 
@@ -39,9 +39,13 @@ module Her
39
39
  superclass.use_api if superclass.respond_to?(:use_api)
40
40
  end
41
41
 
42
- return @_her_use_api unless value
42
+ unless value
43
+ return (@_her_use_api.respond_to? :call) ? @_her_use_api.call : @_her_use_api
44
+ end
45
+
43
46
  @_her_use_api = value
44
47
  end
48
+
45
49
  alias her_api use_api
46
50
  alias uses_api use_api
47
51
 
@@ -46,7 +46,10 @@ module Her
46
46
  @response_errors = parsed_data[:errors]
47
47
 
48
48
  return false if !response.success? || @response_errors.any?
49
- self.changed_attributes.clear if self.changed_attributes.present?
49
+ if self.changed_attributes.present?
50
+ @previously_changed = self.changed_attributes.clone
51
+ self.changed_attributes.clear
52
+ end
50
53
  end
51
54
  end
52
55
  end
@@ -54,6 +57,14 @@ module Her
54
57
  self
55
58
  end
56
59
 
60
+ # Similar to save(), except that ResourceInvalid is raised if the save fails
61
+ def save!
62
+ if !self.save
63
+ raise Her::Errors::ResourceInvalid, self
64
+ end
65
+ self
66
+ end
67
+
57
68
  # Destroy a resource
58
69
  #
59
70
  # @example
@@ -10,7 +10,7 @@ module Her
10
10
  # @user.to_params
11
11
  # # => { :id => 1, :name => 'John Smith' }
12
12
  def to_params
13
- self.class.to_params(self.attributes)
13
+ self.class.to_params(self.attributes, self.changes)
14
14
  end
15
15
 
16
16
  module ClassMethods
@@ -23,8 +23,15 @@ module Her
23
23
  end
24
24
 
25
25
  # @private
26
- def to_params(attributes)
27
- include_root_in_json? ? { included_root_element => attributes.dup.symbolize_keys } : attributes.dup.symbolize_keys
26
+ def to_params(attributes, changes={})
27
+ filtered_attributes = attributes.dup.symbolize_keys
28
+ if her_api.options[:send_only_modified_attributes]
29
+ filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
30
+ hash[attribute] = filtered_attributes[attribute]
31
+ hash
32
+ end
33
+ end
34
+ include_root_in_json? ? { included_root_element => filtered_attributes } : filtered_attributes
28
35
  end
29
36
 
30
37
  # Return or change the value of `include_root_in_json`
@@ -34,15 +41,13 @@ module Her
34
41
  # include Her::Model
35
42
  # include_root_in_json true
36
43
  # end
37
- def include_root_in_json(value = nil)
38
- @_her_include_root_in_json ||= begin
39
- superclass.include_root_in_json if superclass.respond_to?(:include_root_in_json)
40
- end
41
-
42
- return @_her_include_root_in_json unless value
44
+ def include_root_in_json(value)
43
45
  @_her_include_root_in_json = value
44
46
  end
45
- alias include_root_in_json? include_root_in_json
47
+
48
+ def include_root_in_json?
49
+ @_her_include_root_in_json || (superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?)
50
+ end
46
51
 
47
52
  # Return or change the value of `parse_root_in`
48
53
  #
@@ -51,16 +56,14 @@ module Her
51
56
  # include Her::Model
52
57
  # parse_root_in_json true
53
58
  # end
54
- def parse_root_in_json(value = nil, options = {})
55
- @_her_parse_root_in_json ||= begin
56
- superclass.parse_root_in_json if superclass.respond_to?(:parse_root_in_json)
57
- end
58
-
59
- return @_her_parse_root_in_json unless value
59
+ def parse_root_in_json(value, options = {})
60
60
  @_her_parse_root_in_json = value
61
61
  @_her_parse_root_in_json_format = options[:format]
62
62
  end
63
- alias parse_root_in_json? parse_root_in_json
63
+
64
+ def parse_root_in_json?
65
+ @_her_parse_root_in_json || (superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?)
66
+ end
64
67
 
65
68
  # Return or change the value of `request_new_object_on_build`
66
69
  #
@@ -70,14 +73,12 @@ module Her
70
73
  # request_new_object_on_build true
71
74
  # end
72
75
  def request_new_object_on_build(value = nil)
73
- @_her_request_new_object_on_build ||= begin
74
- superclass.request_new_object_on_build if superclass.respond_to?(:request_new_object_on_build)
75
- end
76
-
77
- return @_her_request_new_object_on_build unless value
78
76
  @_her_request_new_object_on_build = value
79
77
  end
80
- alias request_new_object_on_build? request_new_object_on_build
78
+
79
+ def request_new_object_on_build?
80
+ @_her_request_new_object_on_build || (superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?)
81
+ end
81
82
 
82
83
  # Return or change the value of `root_element`. Always defaults to the base name of the class.
83
84
  #
@@ -132,17 +133,17 @@ module Her
132
133
 
133
134
  # @private
134
135
  def included_root_element
135
- include_root_in_json == true ? root_element : include_root_in_json
136
+ include_root_in_json? == true ? root_element : include_root_in_json?
136
137
  end
137
138
 
138
139
  # @private
139
140
  def parsed_root_element
140
- parse_root_in_json == true ? root_element : parse_root_in_json
141
+ parse_root_in_json? == true ? root_element : parse_root_in_json?
141
142
  end
142
143
 
143
144
  # @private
144
145
  def active_model_serializers_format?
145
- @_her_parse_root_in_json_format == :active_model_serializers
146
+ @_her_parse_root_in_json_format == :active_model_serializers || (superclass.respond_to?(:active_model_serializers_format?) && superclass.active_model_serializers_format?)
146
147
  end
147
148
  end
148
149
  end
@@ -94,7 +94,7 @@ module Her
94
94
  unless path.is_a?(String)
95
95
  parameters = path.try(:with_indifferent_access) || parameters
96
96
  path =
97
- if parameters.include?(primary_key) && parameters[primary_key]
97
+ if parameters.include?(primary_key) && parameters[primary_key] && !parameters[primary_key].kind_of?(Array)
98
98
  resource_path.dup
99
99
  else
100
100
  collection_path.dup
@@ -84,6 +84,7 @@ module Her
84
84
  # # Fetched via GET "/users/1" and GET "/users/2"
85
85
  def find(*ids)
86
86
  params = @params.merge(ids.last.is_a?(Hash) ? ids.pop : {})
87
+ ids = Array(params[@parent.primary_key]) if params.key?(@parent.primary_key)
87
88
 
88
89
  results = ids.flatten.compact.uniq.map do |id|
89
90
  resource = nil
@@ -95,6 +96,7 @@ module Her
95
96
  @parent.request(request_params) do |parsed_data, response|
96
97
  if response.success?
97
98
  resource = @parent.new_from_parsed_data(parsed_data)
99
+ resource.instance_variable_set(:@changed_attributes, {})
98
100
  resource.run_callbacks :find
99
101
  else
100
102
  return nil
@@ -1,3 +1,3 @@
1
1
  module Her
2
- VERSION = "0.6.8"
2
+ VERSION = "0.7"
3
3
  end
@@ -7,6 +7,7 @@ describe Her::Middleware::FirstLevelParseJSON do
7
7
  let(:body_with_errors) { "{\"id\": 1, \"name\": \"Tobias Fünke\", \"errors\": { \"name\": [ \"not_valid\", \"should_be_present\" ] }, \"metadata\": 3}" }
8
8
  let(:body_with_malformed_json) { "wut." }
9
9
  let(:body_with_invalid_json) { "true" }
10
+ let(:empty_body) { '' }
10
11
  let(:nil_body) { nil }
11
12
 
12
13
  it "parses body as json" do
@@ -45,6 +46,10 @@ describe Her::Middleware::FirstLevelParseJSON do
45
46
  subject.parse(nil_body)[:data].should eq({})
46
47
  end
47
48
 
49
+ it 'ensures that an empty response returns an empty hash' do
50
+ subject.parse(empty_body)[:data].should eq({})
51
+ end
52
+
48
53
  context 'with status code 204' do
49
54
  it 'returns an empty body' do
50
55
  env = { :status => 204 }
@@ -59,7 +59,7 @@ describe Her::Model::Associations do
59
59
  its([:has_many]) { should eql [{ :name => :comments, :data_key => :user_comments, :default => {}, :class_name => "Post", :path => "/comments", :inverse_of => :admin }] }
60
60
  end
61
61
 
62
- context "signle has_one association" do
62
+ context "single has_one association" do
63
63
  before { Foo::User.has_one :category, :class_name => "Topic", :foreign_key => "topic_id", :data_key => :topic, :default => nil }
64
64
  its([:has_one]) { should eql [{ :name => :category, :data_key => :topic, :default => nil, :class_name => "Topic", :foreign_key => "topic_id", :path => "/category" }] }
65
65
  end
@@ -146,11 +146,11 @@ describe Her::Model::Associations do
146
146
  end
147
147
 
148
148
  it "does not refetch the parents models data if they have been fetched before" do
149
- @user_with_included_data.comments.first.user.fetch.object_id.should == @user_with_included_data.object_id
149
+ @user_with_included_data.comments.first.user.object_id.should == @user_with_included_data.object_id
150
150
  end
151
151
 
152
152
  it "uses the given inverse_of key to set the parent model" do
153
- @user_with_included_data.posts.first.admin.fetch.object_id.should == @user_with_included_data.object_id
153
+ @user_with_included_data.posts.first.admin.object_id.should == @user_with_included_data.object_id
154
154
  end
155
155
 
156
156
  it "fetches data that was not included through has_many" do
@@ -198,8 +198,8 @@ describe Her::Model::Associations do
198
198
  end
199
199
 
200
200
  it "can tell if it has a association" do
201
- @user_without_included_data.has_association?(:unknown_association).should be_false
202
- @user_without_included_data.has_association?(:organization).should be_true
201
+ @user_without_included_data.has_association?(:unknown_association).should be false
202
+ @user_without_included_data.has_association?(:organization).should be true
203
203
  end
204
204
 
205
205
  it "fetches the resource corresponding to a named association" do
@@ -217,6 +217,11 @@ describe Her::Model::Associations do
217
217
  comment.id.should eq(5)
218
218
  end
219
219
 
220
+ it "'s associations responds to #empty?" do
221
+ @user_without_included_data.organization.respond_to?(:empty?).should be_truthy
222
+ @user_without_included_data.organization.should_not be_empty
223
+ end
224
+
220
225
  [:create, :save_existing, :destroy].each do |type|
221
226
  context "after #{type}" do
222
227
  let(:subject) { self.send("user_with_included_data_after_#{type}")}
@@ -244,6 +249,7 @@ describe Her::Model::Associations do
244
249
  builder.use Faraday::Request::UrlEncoded
245
250
  builder.adapter :test do |stub|
246
251
  stub.get("/users/1") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :organization => { :id => 1, :name => "Bluth Company Inc." }, :organization_id => 1 }.to_json] }
252
+ stub.get("/users/4") { |env| [200, {}, { :id => 1, :name => "Tobias Fünke", :organization => { :id => 1, :name => "Bluth Company Inc." } }.to_json] }
247
253
  stub.get("/users/2") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :organization_id => 1 }.to_json] }
248
254
  stub.get("/users/3") { |env| [200, {}, { :id => 2, :name => "Lindsay Fünke", :company => nil }.to_json] }
249
255
  stub.get("/companies/1") { |env| [200, {}, { :id => 1, :name => "Bluth Company" }.to_json] }
@@ -259,6 +265,7 @@ describe Her::Model::Associations do
259
265
  @user_with_included_data = Foo::User.find(1)
260
266
  @user_without_included_data = Foo::User.find(2)
261
267
  @user_with_included_nil_data = Foo::User.find(3)
268
+ @user_with_included_data_but_no_fk = Foo::User.find(4)
262
269
  end
263
270
 
264
271
  it "maps an array of included data through belongs_to" do
@@ -276,6 +283,57 @@ describe Her::Model::Associations do
276
283
  @user_without_included_data.company.id.should == 1
277
284
  @user_without_included_data.company.name.should == "Bluth Company"
278
285
  end
286
+
287
+ it "does not require foreugn key to have nested object" do
288
+ @user_with_included_data_but_no_fk.company.name.should == "Bluth Company Inc."
289
+ end
290
+ end
291
+
292
+ context "object returned by the association method" do
293
+ before do
294
+ spawn_model "Foo::Role" do
295
+ def present?
296
+ "of_course"
297
+ end
298
+ end
299
+ spawn_model "Foo::User" do
300
+ has_one :role
301
+ end
302
+ end
303
+
304
+ let(:associated_value) { Foo::Role.new }
305
+ let(:user_with_role) do
306
+ Foo::User.new.tap { |user| user.role = associated_value }
307
+ end
308
+
309
+ subject { user_with_role.role }
310
+
311
+ it "doesnt mask the object's basic methods" do
312
+ subject.class.should == Foo::Role
313
+ end
314
+
315
+ it "doesnt mask core methods like extend" do
316
+ committer = Module.new
317
+ subject.extend committer
318
+ associated_value.should be_kind_of committer
319
+ end
320
+
321
+ it "can return the association object" do
322
+ subject.association.should be_kind_of Her::Model::Associations::Association
323
+ end
324
+
325
+ it "still can call fetch via the association" do
326
+ subject.association.fetch.should eq associated_value
327
+ end
328
+
329
+ it "calls missing methods on associated value" do
330
+ subject.present?.should == "of_course"
331
+ end
332
+
333
+ it "can use association methods like where" do
334
+ subject.where(role: 'committer').association.
335
+ params.should include :role
336
+ end
279
337
  end
280
338
 
281
339
  context "building and creating association data" do
@@ -318,5 +376,18 @@ describe Her::Model::Associations do
318
376
  @user.comments.should == [@comment]
319
377
  end
320
378
  end
379
+
380
+ context "with #new" do
381
+ it "creates nested models from hash attibutes" do
382
+ user = Foo::User.new(:name => "vic", :comments => [{:text => "hello"}])
383
+ user.comments.first.text.should == "hello"
384
+ end
385
+
386
+ it "assigns nested models if given as already constructed objects" do
387
+ bye = Foo::Comment.new(:text => "goodbye")
388
+ user = Foo::User.new(:name => 'vic', :comments => [bye])
389
+ user.comments.first.text.should == 'goodbye'
390
+ end
391
+ end
321
392
  end
322
393
  end
@@ -7,7 +7,7 @@ describe Her::Model::Attributes do
7
7
 
8
8
  it "handles new resource" do
9
9
  @new_user = Foo::User.new(:fullname => "Tobias Fünke")
10
- @new_user.new?.should be_true
10
+ @new_user.new?.should be_truthy
11
11
  @new_user.fullname.should == "Tobias Fünke"
12
12
  end
13
13
 
@@ -19,17 +19,17 @@ describe Her::Model::Attributes do
19
19
  it "handles method missing for getter" do
20
20
  @new_user = Foo::User.new(:fullname => 'Mayonegg')
21
21
  expect { @new_user.unknown_method_for_a_user }.to raise_error(NoMethodError)
22
- expect { @new_user.fullname }.to_not raise_error(NoMethodError)
22
+ expect { @new_user.fullname }.not_to raise_error()
23
23
  end
24
24
 
25
25
  it "handles method missing for setter" do
26
26
  @new_user = Foo::User.new
27
- expect { @new_user.fullname = "Tobias Fünke" }.to_not raise_error(NoMethodError)
27
+ expect { @new_user.fullname = "Tobias Fünke" }.not_to raise_error()
28
28
  end
29
29
 
30
30
  it "handles method missing for query" do
31
31
  @new_user = Foo::User.new
32
- expect { @new_user.fullname? }.to_not raise_error(NoMethodError)
32
+ expect { @new_user.fullname? }.not_to raise_error()
33
33
  end
34
34
 
35
35
  it "handles respond_to for getter" do
@@ -59,6 +59,12 @@ describe Her::Model::Attributes do
59
59
  @new_user.get_attribute(:unknown_method_for_a_user).should be_nil
60
60
  @new_user.get_attribute(:fullname).should == 'Mayonegg'
61
61
  end
62
+
63
+ it "handles get_attribute for getter with dash" do
64
+ @new_user = Foo::User.new(:'life-span' => '3 years')
65
+ @new_user.get_attribute(:unknown_method_for_a_user).should be_nil
66
+ @new_user.get_attribute(:'life-span').should == '3 years'
67
+ end
62
68
  end
63
69
 
64
70
 
@@ -113,14 +119,14 @@ describe Her::Model::Attributes do
113
119
  end
114
120
 
115
121
  it "returns false for a non-resource with the same data" do
116
- fake_user = stub(:data => { :id => 1, :fullname => "Lindsay Fünke" })
122
+ fake_user = double(:data => { :id => 1, :fullname => "Lindsay Fünke" })
117
123
  user.should_not == fake_user
118
124
  end
119
125
 
120
126
  it "delegates eql? to ==" do
121
127
  other = Object.new
122
128
  user.should_receive(:==).with(other).and_return(true)
123
- user.eql?(other).should be_true
129
+ user.eql?(other).should be_truthy
124
130
  end
125
131
 
126
132
  it "treats equal resources as equal for Array#uniq" do
@@ -249,13 +255,13 @@ describe Her::Model::Attributes do
249
255
 
250
256
  it "byoasses Her's method" do
251
257
  @user = Foo::User.find(1)
252
- @user.document?.should be_false
258
+ @user.document?.should be_falsey
253
259
 
254
260
  @user = Foo::User.find(1)
255
- @user.document?.should be_false
261
+ @user.document?.should be_falsey
256
262
 
257
263
  @user = Foo::User.find(2)
258
- @user.document?.should be_true
264
+ @user.document?.should be_truthy
259
265
  end
260
266
  end
261
267
  end