flexirest 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +37 -0
  7. data/CONTRIBUTING.md +62 -0
  8. data/Gemfile +4 -0
  9. data/Guardfile +9 -0
  10. data/LICENSE.txt +22 -0
  11. data/README.md +846 -0
  12. data/Rakefile +13 -0
  13. data/doc/ActiveRestClient Internals.graffle +1236 -0
  14. data/doc/ActiveRestClient Internals.png +0 -0
  15. data/flexirest.gemspec +39 -0
  16. data/lib/flexirest.rb +25 -0
  17. data/lib/flexirest/base.rb +189 -0
  18. data/lib/flexirest/caching.rb +92 -0
  19. data/lib/flexirest/configuration.rb +209 -0
  20. data/lib/flexirest/connection.rb +103 -0
  21. data/lib/flexirest/connection_manager.rb +36 -0
  22. data/lib/flexirest/headers_list.rb +47 -0
  23. data/lib/flexirest/instrumentation.rb +62 -0
  24. data/lib/flexirest/lazy_association_loader.rb +97 -0
  25. data/lib/flexirest/lazy_loader.rb +23 -0
  26. data/lib/flexirest/logger.rb +67 -0
  27. data/lib/flexirest/mapping.rb +69 -0
  28. data/lib/flexirest/monkey_patching.rb +7 -0
  29. data/lib/flexirest/proxy_base.rb +193 -0
  30. data/lib/flexirest/recording.rb +24 -0
  31. data/lib/flexirest/request.rb +573 -0
  32. data/lib/flexirest/request_delegator.rb +44 -0
  33. data/lib/flexirest/request_filtering.rb +62 -0
  34. data/lib/flexirest/result_iterator.rb +85 -0
  35. data/lib/flexirest/validation.rb +60 -0
  36. data/lib/flexirest/version.rb +3 -0
  37. data/spec/lib/base_spec.rb +389 -0
  38. data/spec/lib/caching_spec.rb +217 -0
  39. data/spec/lib/configuration_spec.rb +234 -0
  40. data/spec/lib/connection_manager_spec.rb +43 -0
  41. data/spec/lib/connection_spec.rb +159 -0
  42. data/spec/lib/headers_list_spec.rb +61 -0
  43. data/spec/lib/instrumentation_spec.rb +58 -0
  44. data/spec/lib/lazy_association_loader_spec.rb +135 -0
  45. data/spec/lib/lazy_loader_spec.rb +25 -0
  46. data/spec/lib/logger_spec.rb +63 -0
  47. data/spec/lib/mapping_spec.rb +52 -0
  48. data/spec/lib/proxy_spec.rb +189 -0
  49. data/spec/lib/recording_spec.rb +34 -0
  50. data/spec/lib/request_filtering_spec.rb +84 -0
  51. data/spec/lib/request_spec.rb +711 -0
  52. data/spec/lib/result_iterator_spec.rb +140 -0
  53. data/spec/lib/validation_spec.rb +113 -0
  54. data/spec/lib/xml_spec.rb +74 -0
  55. data/spec/spec_helper.rb +88 -0
  56. metadata +347 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b157efe52f1bd6f2fed9ef86bc5a62faedb9e445
4
+ data.tar.gz: dedc9e398421a048f823f8c01186aecc94d57ce2
5
+ SHA512:
6
+ metadata.gz: bb78e0f680b36a8d7e64ad708ff26acac2354d5ef881a3174c29b3957da5f3f7875608d008812554028ed3de0ff778ce7d02e7372b933822b68b46ec20a4fa4d
7
+ data.tar.gz: 47d2dfe5f97ee37e93c0c698491e65078cb745220126ce0c52419cb41f7fc2cf3e47ec220975a68635c7cd4babe1ba0ca087a8e90db5079c1c29cea6365307a6
@@ -0,0 +1,16 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,4 @@
1
+ SimpleCov.start do
2
+ minimum_coverage 80
3
+ add_filter '/spec/'
4
+ end
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ - 2.1.0
5
+ - 2.0.0
6
+ - 1.9.3
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ ## 1.2.0
4
+
5
+ Features:
6
+
7
+ - Allows for beta-support for XML APIs as well as JSON ones.
8
+
9
+ Bugfixes:
10
+
11
+ - In order to allow JRuby to work with Flexirest, the hard-coded dependency on Patron has been removed.
12
+
13
+ ## 1.1.10 - 1.1.12
14
+
15
+ Features:
16
+
17
+ - Parallel requests can now be made
18
+
19
+ Bugfixes
20
+
21
+ - Some work around Faraday's weird method naming
22
+ - Start of XML support
23
+ - URL encoding username and password
24
+
25
+ ## 1.0.9
26
+
27
+ Bugfixes
28
+
29
+ - Correctly handling invalid cache expiry times
30
+
31
+ ## 1.0.8
32
+
33
+ Features:
34
+
35
+ - Added Api-Auth for authentication against APIs that use it
36
+ - Supporting array parameter types
37
+ - Relationships for 'has_one' can now be used
@@ -0,0 +1,62 @@
1
+ # Flexirest (ARC) Contributing Guide
2
+
3
+ ## Introduction
4
+
5
+ This project was built at Which? Ltd in the UK, but was released as open source in 2014 under the MIT Licence.
6
+
7
+ We're happy to receive contributions from the community for features and bugfixes and hopefully this guide helps new developers to the project to understand how to get started with the internals of Flexirest.
8
+
9
+ ## Overview
10
+
11
+ ![Component Overview Diagram](https://raw.githubusercontent.com/whichdigital/active-rest-client/master/doc/Flexirest%20Internals.png)
12
+
13
+ ## Components
14
+
15
+ ### Base
16
+ The main class in ARC is `Flexirest::Base`. This includes a number of modules to provide a basic object ready to inherit from to form your own API classes.
17
+
18
+ **Configuration** includes all of the functionality for class and library level configuration (base_url, verbose logging, request format, etc).
19
+
20
+ **Mapping** provides the class methods `get`, `post`, `put`, `delete`, etc and it dynamically defines a method with the given name.
21
+
22
+ **Request Filtering** allows for all/any outbound requests to be altered before transmission. It maintains a list of filters in a class instance variable and applies each one in the defined order to every request.
23
+
24
+ **Validation** implements some very basic ActiveRecord-like validations to field values (presence, numericality, length and custom-blocks).
25
+
26
+ **Caching** adds low level caching functionality to the base class - registering a default cache store and adds the ability to read and write a cached response (which are called from the request).
27
+
28
+ **Recording** allows for developers to record API responses to a file/database for use when creating mock servers.
29
+
30
+ ### Logger
31
+
32
+ This is a simple class that either uses a plain text file or Rails' logger if being used within a Rails project.
33
+
34
+ ### Connection Manager/Connection
35
+
36
+ The principle is that ARC keeps a cached `Connection` to each unique API server and these are kept open using [persistent connections](https://en.wikipedia.org/wiki/HTTP_persistent_connection). The connection for a given `base_url` is created or retrieved from the pool by the `ConnectionManager`.
37
+
38
+ ### Request
39
+
40
+ `Flexirest::Base` instantiates a new `Flexirest::Request` object for each request, and it's up to this object to format the request body, make the request, parse the response, etc.
41
+
42
+ ### HeaderList
43
+
44
+ A `Request` has a list of headers associated, but if the same header is set with different capitalisation (e.g. during a `before_request` filter or in a `proxy`) then it should set the same header, not add a new one. I believe there is a class called something like Rack::Headers which is supposed to do the same thing, but when I came to implement it, it didn't work for me. *This is a candidate for removal/replacement*.
45
+
46
+ ### Lazy *
47
+
48
+ `LazyLoader` is a simple proxy class that takes an `Flexirest::Request` object, has a `method_missing` and `respond_to` pair that when called actually calls `#request` on the request object to make the API call. This is useful if you want to prepare an API object that doesn't make the call unless it's needed (i.e. like ActiveRecord scopes don't execute if they're used within a cached fragment).
49
+
50
+ `LazyAssociationLoader` is a completely different beast. This is used in HAL responses where the association
51
+
52
+ ### ProxyBase
53
+
54
+ Proxying functionality allows authors of an API class to change any aspect of the request on the way out and the response on the way in. It means that you can work with an old code base but interact with a new API, or write your code base to use a new API that isn't available yet and proxy to the old API.
55
+
56
+ It maintains a mapping of method type and URL (either a string for an exact match, or a regular expression for changeable URLs) and executes the first matching mapping.
57
+
58
+ The `passthrough` method initially rebuilds the url (if the URL itself has been changed or any parameters have been changed) and then makes the original request with the new URL/body.
59
+
60
+ ### ResultIterator
61
+
62
+ This acts like a simple `Array` where the JSON response was an array, but it adds in a `parallelise` method for if you need request all the elements of an array (if they're lazy loaded for example) in multiple threads.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in flexirest.gemspec
4
+ gemspec
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: 'bundle exec rspec' do
5
+ watch(%r{^spec/.*/*_spec\.rb$}) { "spec" }
6
+ watch(%r{^lib/.*/(.+)\.rb$}) { "spec" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Which? Ltd
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,846 @@
1
+ # Flexirest
2
+
3
+ [![Build Status](https://travis-ci.org/andyjeffries/flexirest.svg?branch=master)](https://travis-ci.org/andyjeffries/flexirest)
4
+ [![Coverage Status](https://coveralls.io/repos/andyjeffries/flexirest/badge.png)](https://coveralls.io/r/andyjeffries/flexirest)
5
+ [![Code Climate](https://codeclimate.com/github/andyjeffries/flexirest.png)](https://codeclimate.com/github/andyjeffries/flexirest)
6
+ [![Gem Version](https://badge.fury.io/rb/flexirest.png)](http://badge.fury.io/rb/flexirest)
7
+ [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/andyjeffries/flexirest.svg)](http://isitmaintained.com/project/andyjeffries/flexirest "Average time to resolve an issue")
8
+ [![Percentage of issues still open](http://isitmaintained.com/badge/open/andyjeffries/flexirest.svg)](http://isitmaintained.com/project/andyjeffries/flexirest "Percentage of issues still open")
9
+
10
+ This gem is for accessing REST services in an ActiveRecord style. ActiveResource already exists for this, but it doesn't work where the resource naming doesn't follow Rails conventions, it doesn't have in-built caching and it's not as flexible in general.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'flexirest'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install flexirest
27
+
28
+ ## Usage
29
+
30
+ First you need to create your new model class:
31
+
32
+ ```ruby
33
+ # config/environments/production.rb
34
+ MyApp::Application.configure do
35
+ # ...
36
+ config.api_server_url = "https://www.example.com/api/v1"
37
+ end
38
+
39
+ # app/models/person.rb
40
+ class Person < Flexirest::Base
41
+ base_url Rails.application.config.api_server_url
42
+
43
+ get :all, "/people"
44
+ get :find, "/people/:id"
45
+ put :save, "/people/:id"
46
+ post :create, "/people"
47
+ end
48
+ ```
49
+
50
+ Note I've specified the base_url in the class above. This is useful where you want to be explicit or use different APIs for some classes and be explicit. If you have one server that's generally used, you can set it once with a simple line in a `config/initializer/{something}.rb` file:
51
+
52
+ ```ruby
53
+ Flexirest::Base.base_url = "https://www.example.com/api/v1"
54
+ ```
55
+
56
+ Any `base_url` settings in specific classes override this declared default. You can then use your new class like this:
57
+
58
+ ```ruby
59
+ # Create a new person
60
+ @person = Person.create(
61
+ first_name:"John"
62
+ last_name:"Smith"
63
+ )
64
+
65
+ # Find a person (not needed after creating)
66
+ id = @person.id
67
+ @person = Person.find(id)
68
+
69
+ # Update a person
70
+ @person.last_name = "Jones"
71
+ @person.save
72
+
73
+ # Get all people
74
+ @people = Person.all
75
+ @people.each do |person|
76
+ puts "Hi " + person.first_name
77
+ end
78
+ ```
79
+
80
+ If an API returns an array of results and you have [will_paginate](https://rubygems.org/gems/will_paginate) installed then you can call the paginate method to return a particular page of the results (note: this doesn't reduce the load on the server, but it can help with pagination if you have a cached response).
81
+
82
+ ```ruby
83
+ @people = Person.all
84
+ @people.paginate(page: 1, per_page: 10).each do |person|
85
+ puts "You made the first page: " + person.first_name
86
+ end
87
+ ```
88
+
89
+ Note, you can assign to any attribute, whether it exists or not before and read from any attribute (which will return nil if not found). If you pass a string or a number to a method it will assume that it's for the "id" field. Any other field values must be passed as a hash and you can't mix passing a string/number and a hash.
90
+
91
+ ```ruby
92
+ @person = Person.find(1234) # valid
93
+ @person = Person.find("1234") # valid
94
+ @person = Person.find(:id => 1234) # valid
95
+ @person = Person.find(:id => 1234, :name => "Billy") # valid
96
+ @person = Person.find(1234, :name => "Billy") # invalid
97
+ ```
98
+
99
+ You can also call any mapped method as an instance variable which will pass the current attribute set in as parameters (either GET or POST depending on the mapped method type). If the method returns a single instance it will assign the attributes of the calling object and return itself. If the method returns a list of instances, it will only return the list. So, we could rewrite the create call above as:
100
+
101
+ ```ruby
102
+ @person = Person.new
103
+ @person.first_name = "John"
104
+ @person.last_name = "Smith"
105
+ @person.create
106
+ puts @person.id
107
+ ```
108
+
109
+ The response of the #create call set the attributes at that point (any manually set attributes before that point are removed).
110
+
111
+ If you have attributes beginning with a number, Ruby doesn't like this. So, you can use hash style notation to read/write the attributes:
112
+
113
+ ```ruby
114
+ @tv = Tv.find(model:"UE55U8000") # { "properties" : {"3d" : false} }
115
+ puts @tv.properties["3d"]
116
+ @tv.properties["3d"] = true
117
+ ```
118
+
119
+ If you want to debug the response, using inspect on the response object may well be useful. However, if you want a simpler output, then you can call `#to_json` on the response object:
120
+
121
+ ```ruby
122
+ @person = Person.find(email:"something@example.com")
123
+ puts @person.to_json
124
+ ```
125
+
126
+ ## Advanced Features
127
+
128
+ ### Faraday Configuration
129
+
130
+ Flexirest uses Faraday to allow switching HTTP backends, the default is to just use Faraday's default. To change the used backend just set it in the class by setting `adapter` to a Faraday supported adapter symbol.
131
+
132
+ ```ruby
133
+ Flexirest::Base.adapter = :net_http
134
+ # or ...
135
+ Flexirest::Base.adapter = :patron
136
+ ```
137
+
138
+ In versions before 1.2.0 the adapter was hardcoded to `:patron`, so if you want to ensure it still uses Patron, you should set this setting.
139
+
140
+ If you want more control you can pass a **complete** configuration block ("complete" means that the block does not *override* [the default configuration](https://github.com/andyjeffries/flexirest/blob/5b1953d89e26c02ca74f74464ccb7cd4c9439dcc/lib/flexirest/configuration.rb#L184-L201), but rather *replaces* it). For available config variables look into the Faraday documentation.
141
+
142
+ ```ruby
143
+ Flexirest::Base.faraday_config do |faraday|
144
+ faraday.adapter(:net_http)
145
+ faraday.options.timeout = 10
146
+ faraday.headers['User-Agent'] = "Flexirest/#{Flexirest::VERSION}"
147
+ end
148
+ ```
149
+ ### Associations
150
+
151
+ There are two types of association. One assumes when you call a method you actually want it to call the method on a separate class (as that class has other methods that are useful). The other is lazy loading related classes from a separate URL.
152
+
153
+ #### Association Type 1 - Loading Other Classes
154
+
155
+ If the call would return a single instance or a list of instances that should be considered another object, you can also specify this when mapping the method using the `:has_one` or `:has_many` options respectively. It doesn't call anything on that object except for instantiate it, but it does let you have objects of a different class to the one you initially called.
156
+
157
+ ```ruby
158
+ class Expense < Flexirest::Base
159
+ def inc_vat
160
+ ex_vat * 1.20
161
+ end
162
+ end
163
+
164
+ class Address < Flexirest::Base
165
+ def full_string
166
+ return "#{self.street}, #{self.city}, #{self.region}, #{self.country}"
167
+ end
168
+ end
169
+
170
+ class Person < Flexirest::Base
171
+ get :find, "/people/:id", :has_many => {:expenses => Expense}, :has_one => {:address => Address}
172
+ end
173
+
174
+ @person = Person.find(1)
175
+ puts @person.expenses.reduce {|e| e.inc_vat}
176
+ puts @person.address.full_string
177
+ ```
178
+
179
+ #### Association Type 2 - Lazy Loading From Other URLs
180
+
181
+ When mapping the method, passing a list of attributes will cause any requests for those attributes to mapped to the URLs given in their responses. The response for the attribute may be one of the following:
182
+
183
+ ```ruby
184
+ "attribute" : "URL"
185
+ "attribute" : ["URL", "URL"]
186
+ "attribute" : { "url" : "URL"}
187
+ "attribute" : { "href" : "URL"}
188
+ "attribute" : { "something" : "URL"}
189
+ ```
190
+
191
+ The difference between the last 3 examples is that a key of `url` or `href` signifies it's a single object that is lazy loaded from the value specified. Any other keys assume that it's a nested set of URLs (like in the array situation, but accessible via the keys - e.g. object.attribute.something in the above example).
192
+
193
+ It is required that the URL is a complete URL including a protocol starting with "http". To configure this use code like:
194
+
195
+ ```ruby
196
+ class Person < Flexirest::Base
197
+ get :find, "/people/:id", :lazy => [:orders, :refunds]
198
+ end
199
+ ```
200
+
201
+ And use it like this:
202
+
203
+ ```ruby
204
+ # Makes a call to /people/1
205
+ @person = Person.find(1)
206
+
207
+ # Makes a call to the first URL found in the "books":[...] array in the article response
208
+ # only makes the HTTP request when first used though
209
+ @person.books.first.name
210
+ ```
211
+
212
+ #### Association Type 3 - HAL Auto-loaded Resources
213
+
214
+ You don't need to define lazy attributes if they are defined using [HAL](http://stateless.co/hal_specification.html) (with an optional embedded representation). If your resource has an `_links` item (and optionally an `_embedded` item) then it will automatically treat the linked resources (with the `_embedded` cache) as if they were defined using `:lazy` as per type 2 above.
215
+
216
+ If you need to, you can access properties of the HAL association. By default just using the HAL association gets the embedded resource (or requests the remote resource if not available in the `_embedded` list).
217
+
218
+ ```ruby
219
+ @person = Person.find(1)
220
+ @person.students[0]._hal_attributes("title")
221
+ ```
222
+
223
+ #### Association Type 4 - Nested Resources
224
+
225
+ It's common to have resources that are logically children of other resources. For example, suppose that your API includes these endpoints:
226
+
227
+ | HTTP Verb | Path | |
228
+ |-----------|-----------------------------|------------------------------------------|
229
+ | POST | /magazines/:magazine_id/ads | create a new ad belonging to a magazine |
230
+ | GET | /magazines/:magazine_id/ads | display a list of all ads for a magazine |
231
+
232
+ In these cases, your child class will contain the following:
233
+
234
+ ```ruby
235
+ class Ad < Flexirest::Base
236
+ post :create, "/magazines/:magazine_id/ads"
237
+ get :all, "/magazines/:magazine_id/ads"
238
+ end
239
+ ```
240
+
241
+ You can then access Ads by specifying their magazine IDs:
242
+
243
+ ```ruby
244
+ Add.all(magazine_id: 1)
245
+ Add.create(magazine_id: 1, title: "My Add Title")
246
+ ```
247
+
248
+ #### Combined Example
249
+
250
+ OK, so let's say you have an API for getting articles. Each article has a property called `title` (which is a string) and a property `images` which includes a list of URIs. Following this URI would take you to a image API that returns the image's `filename` and `filesize`. We'll also assume this is a HAL compliant API. We would declare our two models (one for articles and one for images) like the following:
251
+
252
+ ```ruby
253
+ class Article < Flexirest::Base
254
+ get :find, '/articles/:id', has_many:{:images => Image} # ,lazy:[:images] isn't needed as we're using HAL
255
+ end
256
+
257
+ class Image < Flexirest::Base
258
+ # You may have mappings here
259
+
260
+ def nice_size
261
+ "#{size/1024}KB"
262
+ end
263
+ end
264
+ ```
265
+
266
+ We assume the /articles/:id call returns something like the following:
267
+
268
+ ```json
269
+ {
270
+ "title": "Fly Fishing",
271
+ "author": "J R Hartley",
272
+ "images": [
273
+ "http://api.example.com/images/1",
274
+ "http://api.example.com/images/2"
275
+ ]
276
+ }
277
+ ```
278
+
279
+ We said above that the /images/:id call would return something like:
280
+
281
+ ```json
282
+ {
283
+ "filename": "http://cdn.example.com/images/foo.jpg",
284
+ "filesize": 123456
285
+ }
286
+ ```
287
+
288
+ When it comes time to use it, you would do something like this:
289
+
290
+ ```ruby
291
+ @article = Article.find(1)
292
+ @article.images.is_a?(Flexirest::LazyAssociationLoader)
293
+ @article.images.size == 2
294
+ @article.images.each do |image|
295
+ puts image.inspect
296
+ end
297
+ ```
298
+
299
+ At this point, only the HTTP call to '/articles/1' has been made. When you actually start using properties of the images list/image object then it makes a call to the URL given in the images list and you can use the properties as if it was a nested JSON object in the original response instead of just a URL:
300
+
301
+ ```ruby
302
+ @image = @article.images.first
303
+ puts @image.filename
304
+ # => http://cdn.example.com/images/foo.jpg
305
+ puts @image.filesize
306
+ # => 123456
307
+ ```
308
+
309
+ You can also treat `@image` looks like an Image class (and you should 100% treat it as one) it's technically a lazy loading proxy. So, if you cache the views for your application should only make HTTP API requests when actually necessary.
310
+
311
+ ```ruby
312
+ puts @image.nice_size
313
+ # => 121KB
314
+ ```
315
+
316
+ ### Caching
317
+
318
+ Expires and ETag based caching is enabled by default, but with a simple line in the application.rb/production.rb you can disable it:
319
+
320
+ ```ruby
321
+ Flexirest::Base.perform_caching = false
322
+ ```
323
+
324
+ or you can disable it per classes with:
325
+
326
+ ```ruby
327
+ class Person < Flexirest::Base
328
+ perform_caching false
329
+ end
330
+ ```
331
+
332
+ If Rails is defined, it will default to using Rails.cache as the cache store, if not, you'll need to configure one with a `ActiveSupport::Cache::Store` compatible object using:
333
+
334
+ ```ruby
335
+ Flexirest::Base.cache_store = Redis::Store.new("redis://localhost:6379/0/cache")
336
+ ```
337
+
338
+ ### Using filters
339
+
340
+ You can use filters to alter get/post parameters, the URL or set the post body (doing so overrides normal parameter insertion in to the body) before a request or to adjust the response after a request. This can either be a block or a named method (like ActionController's `before_filter`/`before_action` methods).
341
+
342
+ The filter is passed the name of the method (e.g. `:save`) and an object (a request object for `before_request` and a response object for `after_request`). The request object has four public attributes `post_params` (a Hash of the POST parameters), `get_params` (a Hash of the GET parameters), headers and `url` (a String containing the full URL without GET parameters appended)
343
+
344
+ ```ruby
345
+ require 'secure_random'
346
+
347
+ class Person < Flexirest::Base
348
+ before_request do |name, request|
349
+ if request.post? || name == :save
350
+ id = request.post_params.delete(:id)
351
+ request.get_params[:id] = id
352
+ end
353
+ end
354
+
355
+ before_request :replace_token_in_url
356
+
357
+ before_request :add_authentication_details
358
+
359
+ before_request :replace_body
360
+
361
+ before_request :override_default_content_type
362
+
363
+ private
364
+
365
+ def replace_token_in_url(name, request)
366
+ request.url.gsub!("#token", SecureRandom.hex)
367
+ end
368
+
369
+ def add_authentication_details(name, request)
370
+ request.headers["X-Custom-Authentication-Token"] = ENV["AUTH_TOKEN"]
371
+ end
372
+
373
+ def replace_body(name, request)
374
+ if name == :create
375
+ request.body = request.post_params.to_json
376
+ end
377
+ end
378
+
379
+ def override_default_content_type(name, request)
380
+ if name == :save
381
+ request.headers["Content-Type"] = "application/json"
382
+ end
383
+ end
384
+ end
385
+ ```
386
+
387
+ If you need to, you can create a custom parent class with a `before_request` filter and all children will inherit this filter.
388
+
389
+ ```ruby
390
+ class MyProject::Base < Flexirest::Base
391
+ before_request do |name, request|
392
+ request.get_params[:api_key] = "1234567890-1234567890"
393
+ end
394
+ end
395
+
396
+ class Person < MyProject::Base
397
+ # No need to declare a before_request for :api_key, already defined by the parent
398
+ end
399
+ ```
400
+
401
+ After filters work in exactly the same way:
402
+
403
+ ```ruby
404
+ class Person < Flexirest::Base
405
+ after_request :fix_empty_content
406
+
407
+ private
408
+
409
+ def fix_empty_content(name, response)
410
+ if response.status == 204 && response.body.blank?
411
+ response.body = '{"empty": true}'
412
+ end
413
+ end
414
+ end
415
+ ```
416
+
417
+ ### Lazy Loading
418
+
419
+ Flexirest supports lazy loading (delaying the actual API call until the response is actually used, so that views can be cached without still causing API calls).
420
+
421
+ **Note: Currently this isn't enabled by default, but this is likely to change in the future to make lazy loading the default.**
422
+
423
+ To enable it, simply call the lazy_load! method in your class definition:
424
+
425
+ ```ruby
426
+ class Article < Flexirest::Base
427
+ lazy_load!
428
+ end
429
+ ```
430
+
431
+ If you have a ResultIterator that has multiple objects, each being lazy loaded or HAL linked resources that isn't loaded until it's used, you can actually parallelise the fetching of the items using code like this:
432
+
433
+ ```ruby
434
+ items.parallelise(:id)
435
+
436
+ # or
437
+
438
+ items.parallelise do |item|
439
+ item.id
440
+ end
441
+ ```
442
+
443
+ This will return an array of the named method for each object or the response from the block and will have loaded the objects in to the resource.
444
+
445
+
446
+ ### Authentication
447
+
448
+ #### Basic
449
+
450
+ You can authenticate with Basic authentication by putting the username and password in to the `base_url` or by setting them within the specific model:
451
+
452
+ ```ruby
453
+ class Person < Flexirest::Base
454
+ username 'api'
455
+ password 'eb693ec-8252c-d6301-02fd0-d0fb7-c3485'
456
+
457
+ # ...
458
+ end
459
+ ```
460
+
461
+ #### Api-Auth
462
+
463
+ Using the [Api-Auth](https://github.com/mgomes/api_auth) integration it is very easy to sign requests. Include the Api-Auth gem in your Gemfile and in then add it to your application. Then simply configure Api-Auth one time in your app and all requests will be signed from then on.
464
+
465
+ ```ruby
466
+ require 'api-auth'
467
+
468
+ @access_id = '123456'
469
+ @secret_key = 'abcdef'
470
+ Flexirest::Base.api_auth_credentials(@access_id, @secret_key)
471
+ ```
472
+
473
+ You can also specify different credentials for different models just like configuring base_url
474
+ ```ruby
475
+ class Person < Flexirest::Base
476
+ api_auth_credentials('123456', 'abcdef')
477
+ end
478
+ ```
479
+
480
+ For more information on how to generate an access id and secret key please read the [Api-Auth](https://github.com/mgomes/api_auth) documentation.
481
+
482
+ ### Body Types
483
+
484
+ By default Flexirest puts the body in to normal CGI parameters in K=V&K2=V2 format. However, if you want to use JSON for your PUT/POST requests, you can use either (the other option, the default, is `:form_encoded`):
485
+
486
+ ```ruby
487
+ class Person < Flexirest::Base
488
+ request_body_type :json
489
+ # ...
490
+ end
491
+ ```
492
+
493
+ or
494
+
495
+ ```ruby
496
+ Flexirest::Base.request_body_type = :json
497
+ ```
498
+
499
+ This will also set the header `Content-Type` to `application/x-www-form-urlencoded` by default or `application/json; charset=utf-8` when `:json`. You can override this using the filter `before_request`.
500
+
501
+ If you have an API that is inconsistent in its body type requirements, you can also specify it on the individual method mapping:
502
+
503
+ ```ruby
504
+ class Person < Flexirest::Base
505
+ request_body_type :form_encoded # This is the default, but just for demo purposes
506
+
507
+ get :all, '/people', request_body_type: :json
508
+ end
509
+ ```
510
+
511
+ ### Parallel Requests
512
+
513
+ Sometimes you know you will need to make a bunch of requests and you don't want to wait for one to finish to start the next. When using parallel requests there is the potential to finish many requests all at the same time taking only as long as the single longest request. To use parallel requests you will need to set Active-Rest-Client to use a Faraday adapter that supports parallel requests [(such as Typhoeus)](https://github.com/lostisland/faraday/wiki/Parallel-requests).
514
+ ```ruby
515
+ # Set adapter to Typhoeus to use parallel requests
516
+ Flexirest::Base.adapter = :typhoeus
517
+ ```
518
+
519
+ Now you just need to get ahold of the connection that is going to make the requests by specifying the same host that the models will be using. When inside the `in_parallel` block call request methods as usual and access the results after the `in_parallel` block ends.
520
+ ```ruby
521
+ Flexirest::ConnectionManager.in_parallel('https://www.example.com') do
522
+ @person = Person.find(1234)
523
+ @employers = Employer.all
524
+
525
+ puts @person #=> nil
526
+ puts @employers #=> nil
527
+ end # The requests are all fired in parallel during this end statement
528
+
529
+ puts @person.name #=> "John"
530
+ puts @employers.size #=> 7
531
+ ```
532
+
533
+ ### Faking Calls
534
+
535
+ There are times when an API hasn't been developed yet, so you want to fake the API call response. To do this, you can simply pass a `fake` option when mapping the call containing the response.
536
+
537
+ ```ruby
538
+ class Person < Flexirest::Base
539
+ get :all, '/people', fake: [{first_name:"Johnny"}, {first_name:"Bob"}]
540
+ end
541
+ ```
542
+
543
+ You may want to run a proc when faking data (to put information from the parameters in to the response or return different responses depending on the parameters). To do this just pass a proc to :fake:
544
+
545
+ ```ruby
546
+ class Person < Flexirest::Base
547
+ get :all, '/people', fake: ->(request) { {result: request.get_params[:id]} }
548
+ end
549
+ ```
550
+
551
+ ### Raw Requests
552
+
553
+ Sometimes you have have a URL that you just want to force through, but have the response handled in the same way as normal objects or you want to have the filters run (say for authentication). The easiest way to do that is to call `_request` on the class:
554
+
555
+ ```ruby
556
+ class Person < Flexirest::Base
557
+ end
558
+
559
+ people = Person._request('http://api.example.com/v1/people') # Defaults to get with no parameters
560
+ # people is a normal Flexirest object, implementing iteration, HAL loading, etc.
561
+
562
+ Person._request('http://api.example.com/v1/people', :post, {id:1234,name:"John"}) # Post with parameters
563
+ ```
564
+
565
+ If you want to use a lazy loaded request instead (so it will create an object that will only call the API if you use it), you can use `_lazy_request` instead of `_request`. If you want you can create a construct that creates and object that lazy loads itself from a given method (rather than a URL):
566
+
567
+ ```ruby
568
+ @person = Person._lazy_request(Person._request_for(:find, 1234))
569
+ ```
570
+
571
+ This initially creates an Flexirest::Request object as if you'd called `Person.find(1234)` which is then passed in to the `_lazy_request` method to return an object that will call the request if any properties are actually used. This may be useful at some point, but it's actually easier to just prefix the `find` method call with `lazy_` like:
572
+
573
+ ```ruby
574
+ @person = Person.lazy_find(1234)
575
+ ```
576
+
577
+ Doing this will try to find a literally mapped method called "lazy_find" and if it fails, it will try to use "find" but instantiate the object lazily.
578
+
579
+ ### Plain Requests
580
+
581
+ If you are already using Flexirest but then want to simply call a normal URL and receive the resulting content as a string (i.e. not going through JSON parsing or instantiating in to an Flexirest::Base descendent) you can use code like this:
582
+
583
+ ```ruby
584
+ class Person < Flexirest::Base
585
+ end
586
+
587
+ people = Person._plain_request('http://api.example.com/v1/people') # Defaults to get with no parameters
588
+ # people is a normal Flexirest object, implementing iteration, HAL loading, etc.
589
+
590
+ Person._plain_request('http://api.example.com/v1/people', :post, {id:1234,name:"John"}) # Post with parameters
591
+ ```
592
+
593
+ The parameters are the same as for `_request`, but it does no parsing on the response
594
+
595
+ ### Proxying APIs
596
+
597
+ Sometimes you may be working with an old API that returns JSON in a less than ideal format or the URL or parameters required have changed. In this case you can define a descendent of `Flexirest::ProxyBase`, pass it to your model as the proxy and have it rework URLs/parameters on the way out and the response on the way back in (already converted to a Ruby hash/array). By default any non-proxied URLs are just passed through to the underlying connection layer. For example:
598
+
599
+ ```ruby
600
+ class ArticleProxy < Flexirest::ProxyBase
601
+ get "/all" do
602
+ url "/all_people" # Equiv to url.gsub!("/all", "/all_people") if you wanted to keep params
603
+ response = passthrough
604
+ translate(response) do |body|
605
+ body["first_name"] = body.delete("fname")
606
+ body
607
+ end
608
+ end
609
+ end
610
+
611
+ class Article < Flexirest::Base
612
+ proxy ArticleProxy
613
+ base_url "http://www.example.com"
614
+
615
+ get :all, "/all", fake:"{\"name\":\"Billy\"}"
616
+ get :list, "/list", fake:"[{\"name\":\"Billy\"}, {\"name\":\"John\"}]"
617
+ end
618
+
619
+ Article.all.first_name == "Billy"
620
+ ```
621
+
622
+ This example does two things:
623
+
624
+ 1. It rewrites the incoming URL for any requests matching "*/all*" to "/all_people"
625
+ 2. It uses the `translate` method to move the "fname" attribute from the response body to be called "first_name". The translate method must return the new object at the end (either the existing object alterered, or a new object to replace it with)
626
+
627
+ As the comment shows, you can use `url value` to set the request URL to a particular value, or you can call `gsub!` on the url to replace parts of it using more complicated regular expressions.
628
+
629
+ You can use the `get_params` or `post_params` methods within your proxy block to amend/create/delete items from those request parameters, like this:
630
+
631
+ ```ruby
632
+ get "/list" do
633
+ get_params["id"] = get_params.delete("identifier")
634
+ passthrough
635
+ end
636
+ ```
637
+
638
+ This example renames the get_parameter for the request from `identifier` to `id` (the same would have worked with post_params if it was a POST/PUT request). The `passthrough` method will take care of automatically recombining them in to the URL or encoding them in to the body as appropriate.
639
+
640
+ If you want to manually set the body for the API yourself you can use the `body` method
641
+
642
+ ```ruby
643
+ put "/update" do
644
+ body "{\"id\":#{post_params["id"]}}"
645
+ passthrough
646
+ end
647
+ ```
648
+
649
+ This example takes the `post_params["id"]` and converts the body from being a normal form-encoded body in to being a JSON body.
650
+
651
+ The proxy block expects one of three things to be the return value of the block.
652
+
653
+ 1. The first options is that the call to `passthrough` is the last thing and it calls down to the connection layer and returns the actual response from the server in to the "API->Object" mapping layer ready for use in your application
654
+ 2. The second option is to save the response from `passthrough` and use `translate` on it to alter the structure.
655
+ 3. The third option is to use `render` if you want to completely fake an API and return the JSON yourself
656
+
657
+ To completely fake the API, you can do the following. Note, this is also achievable using the `fake` setting when mapping a method, however by doing it in a Proxy block means you can dynamically generate the JSON rather than just a hard coded string.
658
+
659
+ ```ruby
660
+ put "/fake" do
661
+ render "{\"id\":1234}"
662
+ end
663
+ ```
664
+
665
+ ### Translating APIs
666
+
667
+ **IMPORTANT: This functionality has been deprecated in favour of the "Proxying APIs" functionality above. You should aim to remove this from your code as soon as possible.**
668
+
669
+ Sometimes you may be working with an API that returns JSON in a less than ideal format. In this case you can define a barebones class and pass it to your model. The Translator class must have class methods that are passed the JSON object and should return an object in the correct format. It doesn't need to have a method unless it's going to translate that mapping though (so in the example below there's no list method). For example:
670
+
671
+ ```ruby
672
+ class ArticleTranslator
673
+ def self.all(object)
674
+ ret = {}
675
+ ret["first_name"] = object["name"]
676
+ ret
677
+ end
678
+ end
679
+
680
+ class Article < Flexirest::Base
681
+ translator ArticleTranslator
682
+ base_url "http://www.example.com"
683
+
684
+ get :all, "/all", fake:"{\"name\":\"Billy\"}"
685
+ get :list, "/list", fake:"[{\"name\":\"Billy\"}, {\"name\":\"John\"}]"
686
+ end
687
+
688
+ Article.all.first_name == "Billy"
689
+ ```
690
+
691
+ ### Default Parameters
692
+
693
+ If you want to specify default parameters you shouldn't use a path like:
694
+
695
+ ```ruby
696
+ class Person < Flexirest::Base
697
+ get :all, '/people?all=true' # THIS IS WRONG!!!
698
+ end
699
+ ```
700
+
701
+ You should use a defaults option to specify the defaults, then they will be correctly overwritten when making the request
702
+
703
+ ```ruby
704
+ class Person < Flexirest::Base
705
+ get :all, '/people', :defaults => {:active => true}
706
+ end
707
+
708
+ @people = Person.all(active:false)
709
+ ```
710
+
711
+ ### HTTP/Parse Error Handling
712
+
713
+ Sometimes the backend server may respond with a non-200/304 header, in which case the code will raise an `Flexirest::HTTPClientException` for 4xx errors or an `Flexirest::HTTPServerException` for 5xx errors. These both have a `status` accessor and a `result` accessor (for getting access to the parsed body):
714
+
715
+ ```ruby
716
+ begin
717
+ Person.all
718
+ rescue Flexirest::HTTPClientException, Flexirest::HTTPServerException => e
719
+ Rails.logger.error("API returned #{e.status} : #{e.result.message}")
720
+ end
721
+ ```
722
+
723
+ If the response is unparsable (e.g. not in the desired content type), then it will raise an `Flexirest::ResponseParseException` which has a `status` accessor for the HTTP status code and a `body` accessor for the unparsed response body.
724
+
725
+ ### Validation
726
+
727
+ You can create validations on your objects just like Rails' built in ActiveModel validations. For example:
728
+
729
+ ```ruby
730
+ class Person < Flexirest::Base
731
+ validates :first_name, presence:true
732
+ validates :password, length:{within:6..12}
733
+ validates :post_code, length:{minimum:6, maximum:8}
734
+ validates :salary, numericality:true, minimum:20_000, maximum:50_000
735
+
736
+ validates :first_name do |object, name, value|
737
+ object.errors[name] << "must be over 4 chars long" if value.length <= 4
738
+ end
739
+
740
+ get :index, '/'
741
+ end
742
+ ```
743
+
744
+ Note the block based validation is responsible for adding errors to `object.errors[name]` (and this will automatically be ready for `<<` inserting into).
745
+
746
+ Validations are run when calling `valid?` or when calling any API on an instance (and then only if it is `valid?` will the API go on to be called).
747
+
748
+ ### Debugging
749
+
750
+ You can turn on verbose debugging to see what is sent to the API server and what is returned in one of these two ways:
751
+
752
+ ```ruby
753
+ class Article < Flexirest::Base
754
+ verbose true
755
+ end
756
+
757
+ class Person < Flexirest::Base
758
+ verbose!
759
+ end
760
+ ```
761
+
762
+ By default verbose logging isn't enabled, so it's up to the developer to enable it (and remember to disable it afterwards). It does use debug level logging, so it shouldn't fill up a correctly configured production server anyway.
763
+
764
+ If you prefer to record the output of an API call in a more automated fashion you can use a callback called `record_response` like this:
765
+
766
+ ```ruby
767
+ class Article < Flexirest::Base
768
+ record_response do |url, response|
769
+ File.open(url.parameterize, "w") do |f|
770
+ f << response.body
771
+ end
772
+ end
773
+ end
774
+ ```
775
+
776
+ ## Beta Features
777
+
778
+ ### XML Responses
779
+
780
+ Flexirest uses Crack to allow parsing of XML responses. For example, given an XML response of (with a content type of `application/xml` or `text/xml`):
781
+
782
+ ```xml
783
+ <?xml version="1.0" encoding="utf-8"?>
784
+ <feed xmlns="http://www.w3.org/2005/Atom">
785
+
786
+ <title>Example Feed</title>
787
+ <link href="http://example.org/"/>
788
+ <updated>2003-12-13T18:30:02Z</updated>
789
+ <author>
790
+ <name>John Doe</name>
791
+ </author>
792
+ <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
793
+
794
+ <entry>
795
+ <title>Atom-Powered Robots Run Amok</title>
796
+ <link href="http://example.org/2003/12/13/atom03"/>
797
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
798
+ <updated>2003-12-13T18:30:02Z</updated>
799
+ <summary>Some text.</summary>
800
+ </entry>
801
+
802
+ <entry>
803
+ <title>Something else cool happened</title>
804
+ <link href="http://example.org/2015/08/11/andyjeffries"/>
805
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6b</id>
806
+ <updated>2015-08-11T18:30:02Z</updated>
807
+ <summary>Some other text.</summary>
808
+ </entry>
809
+
810
+ </feed>
811
+ ```
812
+
813
+ You can use:
814
+
815
+ ```ruby
816
+ class Feed < Flexirest::Base
817
+ base_url "http://www.example.com/v1/"
818
+ get :atom, "/atom"
819
+ end
820
+
821
+ @atom = Feed.atom
822
+
823
+ puts @atom.feed.title
824
+ puts @atom.feed.link.href
825
+ @atom.feed.entry.each do |entry|
826
+ puts "#{entry.title} -> #{entry.link.href}"
827
+ end
828
+ ```
829
+
830
+ If your XML object comes back with a root node and you'd like to ignore it, you can define the mapping as:
831
+
832
+ ```ruby
833
+ class Feed < Flexirest::Base
834
+ get :atom, "/atom", ignore_xml_root: "feed"
835
+ end
836
+ ```
837
+
838
+ For testing purposes, if you are using a `fake` content response when defining your endpoint, you should also provide `fake_content_type: "application/xml"` so that the parser knows to use XML parsing.
839
+
840
+ ## Contributing
841
+
842
+ 1. Fork it
843
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
844
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
845
+ 4. Push to the branch (`git push origin my-new-feature`)
846
+ 5. Create new Pull Request