api_resource 0.6.21 → 0.6.22

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/Gemfile.lock +1 -1
  4. data/LICENSE.txt +22 -0
  5. data/README.md +16 -9
  6. data/docs/Attributes.md +64 -0
  7. data/docs/Caching.md +45 -0
  8. data/docs/GettingStarted.md +149 -0
  9. data/docs/Relationships.md +136 -0
  10. data/docs/ResourceDefinition.md +80 -0
  11. data/docs/Retrieval.md +279 -0
  12. data/docs/Serialization.md +56 -0
  13. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +2 -2
  14. data/lib/api_resource/attributes.rb +16 -4
  15. data/lib/api_resource/base.rb +98 -19
  16. data/lib/api_resource/conditions/abstract_condition.rb +241 -129
  17. data/lib/api_resource/conditions/include_condition.rb +7 -1
  18. data/lib/api_resource/conditions/pagination_condition.rb +37 -0
  19. data/lib/api_resource/conditions/where_condition.rb +19 -0
  20. data/lib/api_resource/conditions.rb +18 -2
  21. data/lib/api_resource/connection.rb +27 -13
  22. data/lib/api_resource/exceptions.rb +11 -11
  23. data/lib/api_resource/finders/abstract_finder.rb +176 -95
  24. data/lib/api_resource/finders/multi_object_association_finder.rb +10 -9
  25. data/lib/api_resource/finders/resource_finder.rb +59 -49
  26. data/lib/api_resource/finders/single_finder.rb +5 -6
  27. data/lib/api_resource/finders/single_object_association_finder.rb +52 -51
  28. data/lib/api_resource/finders.rb +1 -1
  29. data/lib/api_resource/formats/file_upload_format.rb +75 -0
  30. data/lib/api_resource/formats.rb +4 -1
  31. data/lib/api_resource/response.rb +108 -0
  32. data/lib/api_resource/scopes.rb +62 -5
  33. data/lib/api_resource/serializer.rb +1 -1
  34. data/lib/api_resource/typecasters/boolean_typecaster.rb +1 -0
  35. data/lib/api_resource/typecasters/integer_typecaster.rb +1 -0
  36. data/lib/api_resource/typecasters/time_typecaster.rb +12 -4
  37. data/lib/api_resource/version.rb +1 -1
  38. data/lib/api_resource.rb +1 -0
  39. data/spec/lib/associations/has_one_remote_object_proxy_spec.rb +4 -4
  40. data/spec/lib/associations_spec.rb +3 -3
  41. data/spec/lib/attributes_spec.rb +16 -1
  42. data/spec/lib/base_spec.rb +121 -39
  43. data/spec/lib/conditions/{abstract_conditions_spec.rb → abstract_condition_spec.rb} +23 -11
  44. data/spec/lib/conditions/pagination_condition_spec.rb +88 -0
  45. data/spec/lib/finders/multi_object_association_finder_spec.rb +55 -27
  46. data/spec/lib/finders/resource_finder_spec.rb +26 -2
  47. data/spec/lib/finders/single_object_association_finder_spec.rb +14 -6
  48. data/spec/lib/finders_spec.rb +81 -81
  49. data/spec/lib/observing_spec.rb +3 -4
  50. data/spec/lib/response_spec.rb +18 -0
  51. data/spec/lib/scopes_spec.rb +25 -1
  52. data/spec/lib/typecasters/boolean_typecaster_spec.rb +1 -1
  53. data/spec/lib/typecasters/integer_typecaster_spec.rb +1 -1
  54. data/spec/lib/typecasters/time_typecaster_spec.rb +6 -0
  55. data/spec/support/files/bg-awesome.jpg +0 -0
  56. data/spec/support/mocks/test_resource_mocks.rb +26 -16
  57. data/spec/support/requests/test_resource_requests.rb +27 -23
  58. metadata +24 -4
data/docs/Retrieval.md ADDED
@@ -0,0 +1,279 @@
1
+ # Retrieving Records in ApiResource
2
+
3
+
4
+ ## Finding by id
5
+
6
+ The simplest way to retrieve a record is by its ID
7
+
8
+ Resource::Person.find(1)
9
+ # GET /people/1.json
10
+
11
+ ## Simple conditions
12
+
13
+ To add simple query params, you can just use the `.where` method
14
+
15
+ Resource::Person.where(first_name: 'Aaron').all
16
+ # GET /people.json?first_name=Aaron
17
+
18
+ ## Scopes
19
+
20
+ Scopes must be activated in the Server application and are then read by
21
+ the client application via the
22
+ {file:docs/ResourceDefinition.md Resource Definition}.
23
+
24
+ ### Activating a scope in the Server application
25
+
26
+ # app/models/person.rb
27
+ class Person < ActiveRecord::Base
28
+
29
+ include LifebookerCommon::Model::Resource
30
+
31
+ scope :birthday_on, -> date {
32
+ where(birthday: date)
33
+ }
34
+
35
+ scope :aaron, -> {
36
+ where(name: 'Aaron')
37
+ }
38
+
39
+ end
40
+
41
+ ### Using a scope in the Client application
42
+
43
+ # GET /people.json?birthday_on[date]=2014-01-01
44
+ @people = Resource::Person.birthday_on(Date.parse('2014-01-01'))
45
+
46
+ @people.each do |person|
47
+ ...
48
+ end
49
+
50
+ Scopes can be chained together
51
+
52
+ # GET /people.json?birthday_on[date]=2014-01-01&aaron=1
53
+ @people = Resource::Person
54
+ .birthday_on(Date.parse('2014-01-01'))
55
+ .aaron
56
+
57
+ @people.each do |person|
58
+ ...
59
+ end
60
+
61
+ ### Applying scopes in the Client application
62
+
63
+ ApiResource::Base gives us the ability to apply scopes that are valid
64
+ according to the {file:docs/ResourceDefinition.md Resource Definition}
65
+ directly from params that are passed in
66
+
67
+ # In the Client application
68
+ class PeopleController < ApplicationController
69
+
70
+ def index
71
+ @people = Resource::Person.add_scopes(params)
72
+ end
73
+
74
+ end
75
+
76
+ This reads the params supplied to the controller to see if there are any
77
+ valid scopes and then applies them. These scopes follow the same rules
78
+ as the ones that are sent to the server
79
+
80
+ #### Sequence
81
+
82
+ 1. Client supplies params to our client app
83
+
84
+ # curl http://clienthost/people.html?birthday_on[date]=2014-01-01
85
+
86
+ 1. Client controller parses params, passes them to {ApiResource.add_scopes}
87
+ and creates a ScopeCondition to generate params to the server
88
+
89
+ 1. Resource::Person makes a request to the Server application. This request
90
+ has the same parameters as what was passed in to the client application
91
+ assuming those parameters were valid for a scope
92
+
93
+ # GET http://serverhost/people.json?birthday_on[date]=2014-01-01
94
+
95
+ 1. Server application returns the data as JSON and Resource::Person
96
+ instantiates its records for use in the Client application
97
+
98
+ ### Automatically generated scopes
99
+
100
+ `api_resource_server` and {ApiResource::Base} automatically provide several
101
+ scopes for all descendants of ActiveRecord::Base and ApiResource::Base
102
+ respectively
103
+
104
+ #### IDs
105
+
106
+ You can supply an array of ids to a resource to scope by only resources in
107
+ that set of ids
108
+
109
+ # in the Client application
110
+ Resource::Person.add_scopes(ids: [1, 2, 3])
111
+ # => GET /people.json?ids[]=1&ids[]=2&ids[]=3
112
+
113
+ #### Type
114
+
115
+ In order to limit your scope to a subclass of a class that users Single Table
116
+ Inheritance, you can supply the type
117
+
118
+ # in the Server application
119
+ class Carpenter < Person
120
+ end
121
+
122
+ # in the Client application
123
+ Resource::Person.type('Carpenter')
124
+ # => GET /people.json?type=Carpenter
125
+
126
+ # back in the Server application Person.add_scopes
127
+ # converts
128
+
129
+ Person.all
130
+
131
+ # into
132
+
133
+ Carpenter.all
134
+
135
+ #### Page and PerPage
136
+
137
+ In order to paginate, you can pass page and per_page scopes into your
138
+ ApiResource::Base subclass
139
+
140
+ Resource::Person.per_page(20).page(1)
141
+
142
+ # or
143
+
144
+ Resource::Person.add_scopes(per_page: 20, page: 1)
145
+
146
+ # either of these produce
147
+ GET /people.json?page=1&per_page=20
148
+
149
+ In the Server application `add_scopes` will take care of applying
150
+ the pagination using its integration with
151
+ {https://github.com/mislav/will_paginate will_paginate}
152
+
153
+ In addition, the Server application will include a header denoting
154
+ the number of records in the set so that
155
+ {https://github.com/mislav/will_paginate will_paginate} can
156
+ render its page helpers on the client side
157
+
158
+ *Note* that in order to enable this feature the Server application
159
+ *must* use `respond_to`
160
+
161
+ # in a controller in the Client application
162
+ @people = Resource::Person.add_scopes(
163
+ params.merge(page: 2, per_page: 20)
164
+ )
165
+
166
+ # in a view in the Client application
167
+ - @people.each do |person|
168
+ %tr
169
+ %td ...
170
+ # renders the pagination links
171
+ = will_paginate(@people)
172
+
173
+ ### Types of scopes
174
+
175
+ 1. Static scopes
176
+
177
+ # Server code
178
+ scope :x, where(x: 'Val')
179
+ OR
180
+ scope :x, -> { where(x: 'Val') }
181
+
182
+ # Produces request from client
183
+ Resource::Person.x
184
+ # ?x=1
185
+
186
+ 1. Scopes with required parameters
187
+
188
+ # Server code
189
+ scope :birthday_between, -> start, end {
190
+ where("birthday_between ? AND ?", start, end)
191
+ }
192
+
193
+ # Produces request from the client
194
+ @people = Resource::Person.birthday_between(
195
+ start: Date.today,
196
+ end: Date.tomorrow
197
+ )
198
+ # ?birthday_between[start]=2014-01-01&birthday_between[end]=2014-01-02
199
+
200
+ 1. Scopes with varargs
201
+
202
+ # server code
203
+ scope :first_name, -> *names {
204
+ where(first_name: names)
205
+ }
206
+
207
+ # Produces request from the client
208
+ @people = Resource::Person.first_name('Bill', 'Sue')
209
+ # ?first_name[]=Bill&first_name[]=Sue
210
+
211
+ ## Including associated data
212
+
213
+ Oftentimes we will need data from two resources, which would produce n+1 API
214
+ calls unless we sideload our data
215
+
216
+ ### Concept
217
+
218
+ If, for example we loaded a list of 50 People, but needed their State data we
219
+ might have to make 50 requests to the State API endpoint
220
+
221
+ ### Server application
222
+
223
+ # in app/models/person.rb
224
+ class Person < ActiveRecord::Base
225
+
226
+ belongs_to :state
227
+
228
+ # Attributes
229
+ #
230
+ # :first_name, :last_name, :state_id
231
+
232
+ end
233
+
234
+ # in app/models/state.rb
235
+ class State < ActiveRecord::Base
236
+ # Attributes
237
+ #
238
+ # :name
239
+ end
240
+
241
+ # in app/controllers/people_controller.rb
242
+ def index
243
+ respond_with(Person.add_scopes(params))
244
+ end
245
+
246
+ # in app/controllers/states_controller.rb
247
+ def index
248
+ respond_with(State.add_scopes(params))
249
+ end
250
+
251
+ ### Client application
252
+
253
+ # in lib/resource/state.rb
254
+ module Resource
255
+ class State < ApiResource::Base
256
+ end
257
+ end
258
+
259
+ # in lib/resource/person.rb
260
+ module Resource
261
+ class Person < ApiResource::Base
262
+ end
263
+ end
264
+
265
+ # in a controller
266
+ @people = Resource::Person.includes(:state)
267
+
268
+ # this results in
269
+ #
270
+ # GET /people.json
271
+ #
272
+ # and
273
+ #
274
+ # GET /states.json?ids[]=person1.state_id&ids[]=person2.state_id ...
275
+
276
+
277
+ {ApiResource::Base} knows how to include data from any association where
278
+ ids are embedded in the response of the base object and the association
279
+ is present in the {file:docs/ResourceDefinition.md Resource Definition}
@@ -0,0 +1,56 @@
1
+ # Serialization in ApiResource
2
+
3
+ ## Sending Data to Server (JSON)
4
+
5
+ Data is Serialized using singular resource name as a key, and the attributes as the value.
6
+
7
+ # POST /people.json
8
+
9
+ # POST body
10
+ {
11
+ person: {
12
+ first_name: "Aaron",
13
+ last_name: "Burr",
14
+ birthdate: "1755-02-05"
15
+ }
16
+ }
17
+
18
+ ## Retrieving Data From Server
19
+
20
+ ApiResource expects resources to be at the root of the JSON or XML document
21
+ returned
22
+
23
+ # GET /people/1.json
24
+
25
+ # Response
26
+ {
27
+ first_name: 'Aaron',
28
+ last_name: 'Burr',
29
+ birthdate: '1756-02-06'
30
+ }
31
+
32
+ # GET /people.json
33
+
34
+ # Response
35
+ [
36
+ {
37
+ first_name: 'Aaron',
38
+ last_name: 'Burr',
39
+ birthdate: '1756-02-06'
40
+ }
41
+ ]
42
+
43
+ ## Setting the Serializer Format
44
+
45
+ ApiResource ships with two major Formats: {ApiResource::Formats::JsonFormat}
46
+ and {ApiResource::Formats::XmlFormat}
47
+
48
+ The default format is `:json`
49
+
50
+ You can configure your format:
51
+
52
+ ApiResource::Base.format = ApiResource::Formats::JsonFormat
53
+
54
+ # OR
55
+
56
+ ApiResource::Base.format = :xml
@@ -7,7 +7,7 @@ module ApiResource
7
7
  id_method_name = self.foreign_key_name(assoc_name)
8
8
 
9
9
  klass.api_resource_generated_methods.module_eval <<-EOE, __FILE__, __LINE__ + 1
10
-
10
+
11
11
  def #{id_method_name}
12
12
  @attributes_cache[:#{id_method_name}] ||= begin
13
13
  # check our attributes first, then go to the remote
@@ -43,7 +43,7 @@ module ApiResource
43
43
  :ids => associated_ids
44
44
  )
45
45
  # next try for a foreign key e.g. /objects.json?owner_id=1
46
- elsif self.owner.try(:id).present?
46
+ elsif self.owner.try(:id).to_i > 0
47
47
  self.remote_path = self.klass.collection_path(
48
48
  self.owner.class.to_s.foreign_key => self.owner.id
49
49
  )
@@ -136,8 +136,8 @@ module ApiResource
136
136
  # Adds the attribute into some internal data structures but does
137
137
  # not define any methods for it
138
138
  #
139
- # @param arg [Array] A 1 or 2 element array holding an attribute name and
140
- # optionally a type for that attribute
139
+ # @param arg [Array] A 1 or 2 element array holding an
140
+ # attribute name and optionally a type for that attribute
141
141
  # @param access_level [Symbol] Either :protected or :public based on
142
142
  # the access level for this attribute
143
143
  #
@@ -542,9 +542,21 @@ module ApiResource
542
542
 
543
543
  def method_missing(sym, *args, &block)
544
544
  sym = sym.to_sym
545
- if @attributes.keys.symbolize_array.include?(sym)
545
+
546
+ # Maybe the resource definition sucks...
547
+ if self.class.resource_definition_is_invalid?
548
+ self.class.reload_resource_definition
549
+ end
550
+
551
+ # If we don't respond by now...
552
+ if self.respond_to?(sym)
553
+ return self.send(sym, *args, &block)
554
+ elsif @attributes.keys.symbolize_array.include?(sym)
555
+ # Try returning the attributes from the attributes hash
546
556
  return @attributes[sym]
547
557
  end
558
+
559
+ # Fall back to class method_missing
548
560
  super
549
561
  end
550
562
 
@@ -680,4 +692,4 @@ module ApiResource
680
692
 
681
693
  end
682
694
 
683
- end
695
+ end
@@ -31,22 +31,31 @@ module ApiResource
31
31
  # => 7) Write documentation
32
32
  # => 8) Write Examples
33
33
 
34
- class_attribute :site, :proxy, :user, :password, :auth_type, :format,
35
- :timeout, :open_timeout, :ssl_options, :token, :ttl
34
+ class_attribute(
35
+ :site,
36
+ :proxy,
37
+ :user,
38
+ :password,
39
+ :auth_type,
40
+ :format,
41
+ :timeout,
42
+ :open_timeout,
43
+ :ssl_options,
44
+ :token,
45
+ :ttl,
46
+ { instance_writer: false, instance_reader: false }
47
+ )
48
+ self.format = ApiResource::Formats::JsonFormat
36
49
 
37
- class_attribute :include_root_in_json
50
+ class_attribute(:include_root_in_json)
38
51
  self.include_root_in_json = true
39
52
 
40
- class_attribute :include_nil_attributes_on_create
53
+ class_attribute(:include_nil_attributes_on_create)
41
54
  self.include_nil_attributes_on_create = false
42
55
 
43
- class_attribute :include_all_attributes_on_update
56
+ class_attribute(:include_all_attributes_on_update)
44
57
  self.include_nil_attributes_on_create = false
45
58
 
46
- class_attribute :format
47
- self.format = ApiResource::Formats::JsonFormat
48
-
49
-
50
59
  delegate :logger, to: ApiResource
51
60
 
52
61
  class << self
@@ -197,12 +206,12 @@ module ApiResource
197
206
  # backwards compatibility
198
207
  alias_method :reload_class_attributes, :reload_resource_definition
199
208
 
200
- #
209
+ #
201
210
  # Mutex so that multiple Threads don't try to load the resource
202
211
  # definition at the same time
203
- #
212
+ #
204
213
  # @return [Mutex]
205
- def resource_definition_mutex
214
+ def resource_definition_mutex
206
215
  @resource_definition_mutex ||= Mutex.new
207
216
  end
208
217
 
@@ -365,11 +374,15 @@ module ApiResource
365
374
 
366
375
  # path to find
367
376
  def new_element_path(prefix_options = {})
368
- File.join(
377
+ url = File.join(
369
378
  self.prefix(prefix_options),
370
379
  self.collection_name,
371
380
  "new.#{format.extension}"
372
381
  )
382
+ if self.superclass != ApiResource::Base && self.name.present?
383
+ url = "#{url}?type=#{self.name.demodulize}"
384
+ end
385
+ return url
373
386
  end
374
387
 
375
388
  def collection_path(prefix_options = {}, query_options = nil)
@@ -541,6 +554,14 @@ module ApiResource
541
554
  self.class.instantiate_record(self.attributes)
542
555
  end
543
556
 
557
+ #
558
+ # Implementation of to_key for use in Rails forms
559
+ #
560
+ # @return [Array<Fixnum>,nil] Array wrapped id or nil
561
+ def to_key
562
+ [self.id] if self.id.present?
563
+ end
564
+
544
565
  def update_attributes(attrs)
545
566
  self.attributes = attrs
546
567
  self.save
@@ -697,9 +718,14 @@ module ApiResource
697
718
  path = self.collection_path
698
719
  body = self.setup_create_call(*args)
699
720
  headers = self.class.headers
700
- # make the post call
701
- connection.post(path, body, headers).tap do |response|
702
- load_attributes_from_response(response)
721
+
722
+ # this checks to see if we have uploaded a file
723
+ # and sets headers accordingly
724
+ self.with_uploaded_file_format do
725
+ # make the post call
726
+ connection.post(path, body, headers).tap do |response|
727
+ load_attributes_from_response(response)
728
+ end
703
729
  end
704
730
  end
705
731
 
@@ -724,9 +750,14 @@ module ApiResource
724
750
  path = self.element_path(self.id)
725
751
  body = self.setup_update_call(*args)
726
752
  headers = self.class.headers
727
- # We can just ignore the response
728
- connection.put(path, body, headers).tap do |response|
729
- load_attributes_from_response(response)
753
+
754
+ # this checks to see if we have uploaded a file
755
+ # and sets headers accordingly
756
+ self.with_uploaded_file_format do
757
+ # We can just ignore the response
758
+ connection.put(path, body, headers).tap do |response|
759
+ load_attributes_from_response(response)
760
+ end
730
761
  end
731
762
  end
732
763
 
@@ -765,6 +796,54 @@ module ApiResource
765
796
  return data
766
797
  end
767
798
 
799
+ protected
800
+
801
+ #
802
+ # Are any of our attributes of type File?
803
+ #
804
+ # @return [Boolean]
805
+ def has_file_attribute?
806
+ self.attributes.values.any?{|val|
807
+ HTTP::Message.file?(val) ||
808
+ val.is_a?(ActionDispatch::Http::UploadedFile)
809
+ }
810
+ end
811
+
812
+ #
813
+ # Run a block with a given format
814
+ #
815
+ # @param new_format [Module] Format module
816
+ # @param &block [Proc] Block to run
817
+ #
818
+ # @return [Mixed]
819
+ def with_format(new_format, &block)
820
+
821
+ old_format = self.class.format
822
+
823
+ begin
824
+ self.class.format = new_format
825
+ yield
826
+ ensure
827
+ self.class.format = old_format
828
+ end
829
+ end
830
+
831
+ #
832
+ # Wrapper to add the FileUpload format
833
+ #
834
+ # @param &block [Proc] Block to run
835
+ #
836
+ # @return [Mixed]
837
+ def with_uploaded_file_format(&block)
838
+ if self.has_file_attribute?
839
+ self.with_format(Formats::FileUploadFormat) do
840
+ yield
841
+ end
842
+ else
843
+ yield
844
+ end
845
+ end
846
+
768
847
  private
769
848
 
770
849
  def split_options(options = {})