api_resource 0.6.21 → 0.6.22

Sign up to get free protection for your applications and to get access to all the features.
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 = {})