her 0.7 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4f5ffa339d778a9e0c1bdf8284bb6df2452584b9
4
- data.tar.gz: f37e2a93474b2290a0b676ec70c793232ad49eb7
3
+ metadata.gz: 7626abb2e0aa83555379ffb426c47f766e7e5175
4
+ data.tar.gz: 9036b8f208b4e9462740eb22fa1b6d21ef48b9c1
5
5
  SHA512:
6
- metadata.gz: 9d6670525bc4efb9c658cf9692ffda8fdb2c9ae24267f0b44c48980b39330b9b3fd51f47b73ad024f8cbb53f45ebff43d8adafe5adf6cae91f739a17aa5715bd
7
- data.tar.gz: 45e82cff66d8f55e5887769a103a8015deff104c27e0d9e1fc0daff79dd53bc9e67681437ff3b692017d7c74596a9a2b3d5cd133906db0cbec9a01a3874057bb
6
+ metadata.gz: d6a8734624af88d8261d6f26147c517626530c925f886770b284471dde1f9903c455dd77a1dd6e7d21fc8a720d98d293fed0f3f967ccc3516dc05a1528a58b70
7
+ data.tar.gz: 4badd683f1cc2550dc34758573c6a57b46843b52b1a12b213eb917b6b2c60542cb05ff47038c9cc5aa8d497571fbf8569432a7d56024e7109c83fcf200dedc5b
data/README.md CHANGED
@@ -576,10 +576,28 @@ class User
576
576
  end
577
577
 
578
578
  user = Users.find(1)
579
- # GET "/users/1", response is { "user": { "id": 1, "fullname": "Lindsay Fünke"} }
579
+ # GET "/users/1", response is { "user": { "id": 1, "fullname": "Lindsay Fünke" } }
580
580
 
581
581
  users = Users.all
582
- # GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke"}] }
582
+ # GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }, { "id": 1, "fullname": "Tobias Fünke" }] }
583
+ ```
584
+
585
+ #### JSON API support
586
+
587
+ If the API returns data in the [JSON API format](http://jsonapi.org/) you need
588
+ to configure Her as follows:
589
+
590
+ ```ruby
591
+ class User
592
+ include Her::Model
593
+ parse_root_in_json true, format: :json_api
594
+ end
595
+
596
+ user = Users.find(1)
597
+ # GET "/users/1", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }] }
598
+
599
+ users = Users.all
600
+ # GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }, { "id": 2, "fullname": "Tobias Fünke" }] }
583
601
  ```
584
602
 
585
603
  ### Custom requests
@@ -964,6 +982,8 @@ These [fine folks](https://github.com/remiprev/her/contributors) helped with Her
964
982
  * [@aclevy](https://github.com/aclevy)
965
983
  * [@stevschmid](https://github.com/stevschmid)
966
984
  * [@prognostikos](https://github.com/prognostikos)
985
+ * [@dturnerTS](https://github.com/dturnerTS)
986
+ * [@kritik](https://github.com/kritik)
967
987
 
968
988
  ## License
969
989
 
data/lib/her/model.rb CHANGED
@@ -40,6 +40,7 @@ module Her
40
40
  # Supported ActiveModel modules
41
41
  include ActiveModel::AttributeMethods
42
42
  include ActiveModel::Validations
43
+ include ActiveModel::Validations::Callbacks
43
44
  include ActiveModel::Conversion
44
45
  include ActiveModel::Dirty
45
46
  include ActiveModel::Naming
@@ -49,6 +49,8 @@ module Her
49
49
  # user = User.find(1)
50
50
  # new_comment = user.comments.build(:body => "Hello!")
51
51
  # new_comment # => #<Comment user_id=1 body="Hello!">
52
+ # TODO: This only merges the id of the parents, handle the case
53
+ # where this is more deeply nested
52
54
  def build(attributes = {})
53
55
  @klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
54
56
  end
@@ -89,7 +91,8 @@ module Her
89
91
 
90
92
  # @private
91
93
  def assign_nested_attributes(attributes)
92
- @parent.attributes[@name] = Her::Model::Attributes.initialize_collection(@klass, :data => attributes)
94
+ data = attributes.is_a?(Hash) ? attributes.values : attributes
95
+ @parent.attributes[@name] = Her::Model::Attributes.initialize_collection(@klass, :data => data)
93
96
  end
94
97
  end
95
98
  end
@@ -68,7 +68,7 @@ module Her
68
68
  path = build_request_path_from_string_or_symbol(path, params)
69
69
  params = to_params(params) unless #{method.to_sym.inspect} == :get
70
70
  send(:'#{method}_raw', path, params) do |parsed_data, response|
71
- if parsed_data[:data].is_a?(Array) || active_model_serializers_format?
71
+ if parsed_data[:data].is_a?(Array) || active_model_serializers_format? || json_api_format?
72
72
  new_collection(parsed_data)
73
73
  else
74
74
  new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
@@ -19,19 +19,54 @@ module Her
19
19
  # @param [Hash] data
20
20
  # @private
21
21
  def parse(data)
22
- parse_root_in_json? ? data.fetch(parsed_root_element) { data } : data
22
+ if parse_root_in_json? && root_element_included?(data)
23
+ if json_api_format?
24
+ data.fetch(parsed_root_element).first
25
+ else
26
+ data.fetch(parsed_root_element) { data }
27
+ end
28
+ else
29
+ data
30
+ end
23
31
  end
24
32
 
25
33
  # @private
26
34
  def to_params(attributes, changes={})
27
35
  filtered_attributes = attributes.dup.symbolize_keys
36
+ filtered_attributes.merge!(embeded_params(attributes))
28
37
  if her_api.options[:send_only_modified_attributes]
29
38
  filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
30
39
  hash[attribute] = filtered_attributes[attribute]
31
40
  hash
32
41
  end
33
42
  end
34
- include_root_in_json? ? { included_root_element => filtered_attributes } : filtered_attributes
43
+
44
+ if include_root_in_json?
45
+ if json_api_format?
46
+ { included_root_element => [filtered_attributes] }
47
+ else
48
+ { included_root_element => filtered_attributes }
49
+ end
50
+ else
51
+ filtered_attributes
52
+ end
53
+ end
54
+
55
+
56
+ # @private
57
+ # TODO: Handle has_one
58
+ 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 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
69
+ end
35
70
  end
36
71
 
37
72
  # Return or change the value of `include_root_in_json`
@@ -41,30 +76,33 @@ module Her
41
76
  # include Her::Model
42
77
  # include_root_in_json true
43
78
  # end
44
- def include_root_in_json(value)
79
+ def include_root_in_json(value, options = {})
45
80
  @_her_include_root_in_json = value
81
+ @_her_include_root_in_json_format = options[:format]
46
82
  end
47
83
 
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
51
-
52
- # Return or change the value of `parse_root_in`
84
+ # Return or change the value of `parse_root_in_json`
53
85
  #
54
86
  # @example
55
87
  # class User
56
88
  # include Her::Model
57
89
  # parse_root_in_json true
58
90
  # end
91
+ #
92
+ # class User
93
+ # include Her::Model
94
+ # parse_root_in_json true, format: :active_model_serializers
95
+ # end
96
+ #
97
+ # class User
98
+ # include Her::Model
99
+ # parse_root_in_json true, format: :json_api
100
+ # end
59
101
  def parse_root_in_json(value, options = {})
60
102
  @_her_parse_root_in_json = value
61
103
  @_her_parse_root_in_json_format = options[:format]
62
104
  end
63
105
 
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
67
-
68
106
  # Return or change the value of `request_new_object_on_build`
69
107
  #
70
108
  # @example
@@ -76,10 +114,6 @@ module Her
76
114
  @_her_request_new_object_on_build = value
77
115
  end
78
116
 
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
82
-
83
117
  # Return or change the value of `root_element`. Always defaults to the base name of the class.
84
118
  #
85
119
  # @example
@@ -93,12 +127,26 @@ module Her
93
127
  # user.name # => "Tobias"
94
128
  def root_element(value = nil)
95
129
  if value.nil?
96
- @_her_root_element ||= self.name.split("::").last.underscore.to_sym
130
+ if json_api_format?
131
+ @_her_root_element ||= self.name.split("::").last.pluralize.underscore.to_sym
132
+ else
133
+ @_her_root_element ||= self.name.split("::").last.underscore.to_sym
134
+ end
97
135
  else
98
136
  @_her_root_element = value.to_sym
99
137
  end
100
138
  end
101
139
 
140
+ # @private
141
+ def root_element_included?(data)
142
+ data.keys.to_s.include? @_her_root_element.to_s
143
+ end
144
+
145
+ # @private
146
+ def included_root_element
147
+ include_root_in_json? == true ? root_element : include_root_in_json?
148
+ end
149
+
102
150
  # Extract an array from the request data
103
151
  #
104
152
  # @example
@@ -118,8 +166,10 @@ module Her
118
166
  #
119
167
  # users = User.all # [ { :id => 1, :name => "Tobias" } ]
120
168
  # users.first.name # => "Tobias"
169
+ #
170
+ # @private
121
171
  def extract_array(request_data)
122
- if active_model_serializers_format?
172
+ if active_model_serializers_format? || json_api_format?
123
173
  request_data[:data][pluralized_parsed_root_element]
124
174
  else
125
175
  request_data[:data]
@@ -131,11 +181,6 @@ module Her
131
181
  parsed_root_element.to_s.pluralize.to_sym
132
182
  end
133
183
 
134
- # @private
135
- def included_root_element
136
- include_root_in_json? == true ? root_element : include_root_in_json?
137
- end
138
-
139
184
  # @private
140
185
  def parsed_root_element
141
186
  parse_root_in_json? == true ? root_element : parse_root_in_json?
@@ -145,6 +190,26 @@ module Her
145
190
  def active_model_serializers_format?
146
191
  @_her_parse_root_in_json_format == :active_model_serializers || (superclass.respond_to?(:active_model_serializers_format?) && superclass.active_model_serializers_format?)
147
192
  end
193
+
194
+ # @private
195
+ def json_api_format?
196
+ @_her_parse_root_in_json_format == :json_api || (superclass.respond_to?(:json_api_format?) && superclass.json_api_format?)
197
+ end
198
+
199
+ # @private
200
+ def request_new_object_on_build?
201
+ @_her_request_new_object_on_build || (superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?)
202
+ end
203
+
204
+ # @private
205
+ def include_root_in_json?
206
+ @_her_include_root_in_json || (superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?)
207
+ end
208
+
209
+ # @private
210
+ def parse_root_in_json?
211
+ @_her_parse_root_in_json || (superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?)
212
+ end
148
213
  end
149
214
  end
150
215
  end
data/lib/her/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Her
2
- VERSION = "0.7"
2
+ VERSION = "0.7.1"
3
3
  end
@@ -111,7 +111,7 @@ describe Her::Model::Associations do
111
111
  end
112
112
 
113
113
  spawn_model "Foo::User" do
114
- has_many :comments
114
+ has_many :comments, class_name: "Foo::Comment"
115
115
  has_one :role
116
116
  belongs_to :organization
117
117
  has_many :posts, :inverse_of => :admin
@@ -222,6 +222,12 @@ describe Her::Model::Associations do
222
222
  @user_without_included_data.organization.should_not be_empty
223
223
  end
224
224
 
225
+ it 'includes has_many relationships in params by default' do
226
+ params = @user_with_included_data.to_params
227
+ params[:comments].should be_kind_of(Array)
228
+ params[:comments].length.should eq(2)
229
+ end
230
+
225
231
  [:create, :save_existing, :destroy].each do |type|
226
232
  context "after #{type}" do
227
233
  let(:subject) { self.send("user_with_included_data_after_#{type}")}
@@ -96,6 +96,34 @@ describe Her::Model::NestedAttributes do
96
96
  end
97
97
  end
98
98
 
99
+ context "with a has_many association as a Hash" do
100
+ before do
101
+ Her::API.setup :url => "https://api.example.com" do |builder|
102
+ builder.use Her::Middleware::FirstLevelParseJSON
103
+ builder.use Faraday::Request::UrlEncoded
104
+ end
105
+
106
+ spawn_model "Foo::User" do
107
+ has_many :pets
108
+ accepts_nested_attributes_for :pets
109
+ end
110
+
111
+ spawn_model "Foo::Pet"
112
+
113
+ @user_with_data_through_nested_attributes_as_hash = Foo::User.new :name => "Test", :pets_attributes => { '0' => { :name => "Hasi" }, '1' => { :name => "Rodriguez" }}
114
+ end
115
+
116
+ context "when children do not yet exist" do
117
+ it "creates an instance of the associated class" do
118
+ @user_with_data_through_nested_attributes_as_hash.pets.length.should == 2
119
+ @user_with_data_through_nested_attributes_as_hash.pets[0].should be_a(Foo::Pet)
120
+ @user_with_data_through_nested_attributes_as_hash.pets[1].should be_a(Foo::Pet)
121
+ @user_with_data_through_nested_attributes_as_hash.pets[0].name.should == "Hasi"
122
+ @user_with_data_through_nested_attributes_as_hash.pets[1].name.should == "Rodriguez"
123
+ end
124
+ end
125
+ end
126
+
99
127
  context "with an unknown association" do
100
128
  it "raises an error" do
101
129
  expect {
@@ -231,6 +231,95 @@ describe Her::Model::Parse do
231
231
  end
232
232
  end
233
233
 
234
+ context "when parse_root_in_json set json_api to true" do
235
+ before do
236
+ Her::API.setup :url => "https://api.example.com" do |builder|
237
+ builder.use Her::Middleware::FirstLevelParseJSON
238
+ builder.use Faraday::Request::UrlEncoded
239
+ builder.adapter :test do |stub|
240
+ stub.get("/users") { |env| [200, {}, { :users => [{ :id => 1, :fullname => "Lindsay Fünke" }] }.to_json] }
241
+ stub.get("/users/admins") { |env| [200, {}, { :users => [{ :id => 1, :fullname => "Lindsay Fünke" }] }.to_json] }
242
+ stub.get("/users/1") { |env| [200, {}, { :users => [{ :id => 1, :fullname => "Lindsay Fünke" }] }.to_json] }
243
+ stub.post("/users") { |env| [200, {}, { :users => [{ :fullname => "Lindsay Fünke" }] }.to_json] }
244
+ stub.put("/users/1") { |env| [200, {}, { :users => [{ :id => 1, :fullname => "Tobias Fünke Jr." }] }.to_json] }
245
+ end
246
+ end
247
+
248
+ spawn_model("Foo::User") do
249
+ parse_root_in_json true, :format => :json_api
250
+ include_root_in_json true
251
+ custom_get :admins
252
+ end
253
+ end
254
+
255
+ it "parse the data from the JSON root element after .create" do
256
+ @new_user = Foo::User.create(:fullname => "Lindsay Fünke")
257
+ @new_user.fullname.should == "Lindsay Fünke"
258
+ end
259
+
260
+ it "parse the data from the JSON root element after an arbitrary HTTP request" do
261
+ @new_user = Foo::User.admins
262
+ @new_user.first.fullname.should == "Lindsay Fünke"
263
+ end
264
+
265
+ it "parse the data from the JSON root element after .all" do
266
+ @users = Foo::User.all
267
+ @users.first.fullname.should == "Lindsay Fünke"
268
+ end
269
+
270
+ it "parse the data from the JSON root element after .find" do
271
+ @user = Foo::User.find(1)
272
+ @user.fullname.should == "Lindsay Fünke"
273
+ end
274
+
275
+ it "parse the data from the JSON root element after .save" do
276
+ @user = Foo::User.find(1)
277
+ @user.fullname = "Tobias Fünke"
278
+ @user.save
279
+ @user.fullname.should == "Tobias Fünke Jr."
280
+ end
281
+
282
+ it "parse the data from the JSON root element after new/save" do
283
+ @user = Foo::User.new
284
+ @user.fullname = "Lindsay Fünke (before save)"
285
+ @user.save
286
+ @user.fullname.should == "Lindsay Fünke"
287
+ end
288
+ end
289
+
290
+ context "when include_root_in_json set json_api" do
291
+ before do
292
+ Her::API.setup :url => "https://api.example.com" do |builder|
293
+ builder.use Her::Middleware::FirstLevelParseJSON
294
+ builder.use Faraday::Request::UrlEncoded
295
+ end
296
+
297
+ Her::API.default_api.connection.adapter :test do |stub|
298
+ stub.post("/users") { |env| [200, {}, { :users => [{ :id => 1, :fullname => params(env)[:users][:fullname] }] }.to_json] }
299
+ end
300
+ end
301
+
302
+ context "to true" do
303
+ before do
304
+ spawn_model "Foo::User" do
305
+ include_root_in_json true
306
+ parse_root_in_json true, format: :json_api
307
+ custom_post :admins
308
+ end
309
+ end
310
+
311
+ it "wraps params in the element name in `to_params`" do
312
+ @new_user = Foo::User.new(:fullname => "Tobias Fünke")
313
+ @new_user.to_params.should == { :users => [{ :fullname => "Tobias Fünke" }] }
314
+ end
315
+
316
+ it "wraps params in the element name in `.where`" do
317
+ @new_user = Foo::User.where(:fullname => "Tobias Fünke").build
318
+ @new_user.fullname.should == "Tobias Fünke"
319
+ end
320
+ end
321
+ end
322
+
234
323
  context 'when send_only_modified_attributes is set' do
235
324
  before do
236
325
  Her::API.setup :url => "https://api.example.com", :send_only_modified_attributes => true do |builder|
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.7'
4
+ version: 0.7.1
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: 2014-06-03 00:00:00.000000000 Z
11
+ date: 2014-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake