clientele 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7a96006c58600f3a0e961f8c5798038eb69c8286
4
- data.tar.gz: ee0b2685b429d42b87ee27cdaf60cc69f762c708
3
+ metadata.gz: 88fca56c3d0d1aeefda397db16761647de0076d7
4
+ data.tar.gz: e8681f282556c70e74b3390c5c6407a333c82d08
5
5
  SHA512:
6
- metadata.gz: ff8ee67c48d38979c2e12b298adc61ed60f99652468ba93f85d5e75373b948c63f3b2c0e23d5bf4e583877d115d3be9057eee7e9bc28d0ac614c824b1e23159f
7
- data.tar.gz: ec40b35950efe963177f21d3d33bfd539c5b2d1cafccc317ba02569c3889693c992efcf5f0877f94320f63eab4dcc29a095283c7a63f6b67bfd573066847eada
6
+ metadata.gz: ffd27f7cac3344386bb4873fc2ca5407e4ff5e24a5986f3973f13371b980202818940f452b44fc11c2dad0dc2dea07ac7af61c8f99ba1a307b70bc01364ca7de
7
+ data.tar.gz: 982447bbb8ce83d68e0278eb5e264c9c978134d262df35d8bde3cf8c65024fd9857c33cea11644a5fb1e994b81594b242ccd0b60cee8af187afab6361ef5adb2
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in clientele.gemspec
3
+ # Specify your gem's dependencies in rx-norm.gemspec
4
4
  gemspec
5
+
6
+ gem 'pry'
data/README.md CHANGED
@@ -1,27 +1,321 @@
1
1
  Clientele
2
2
  =========
3
3
 
4
- > *DSL for creating RESTful API clients for external services.*
4
+ > *Create Ruby API clients with ease.*
5
5
 
6
- ## Installation
7
6
 
8
- Add this line to your application's Gemfile:
9
7
 
10
- gem 'clientele'
8
+ Usage
9
+ -----
11
10
 
12
- And then execute:
11
+ Clientele makes it easy to create feature-rich ruby API clients with minimal boilerplate.
13
12
 
14
- $ bundle
13
+ In this example, we'll be making an API client for our popular new service at example.com.
15
14
 
16
- Or install it yourself as:
17
15
 
18
- $ gem install clientele
16
+ ### Creating an API Client
19
17
 
20
- ## Usage
18
+ Since we'll be distributing this client as a gem, we'll use bundler to get our boilerplate code set up:
21
19
 
22
- TODO: Write usage instructions here
20
+ ```bash
21
+ bundle gem example-api
22
+ cd example-api
23
+ ```
23
24
 
24
- ## Contributing
25
+ Next we'll add `clientele` as a dependency to our gemspec.
26
+
27
+ ```ruby
28
+ # example-api.gemspec
29
+
30
+ Gem::Specification.new do |spec|
31
+
32
+ # ...
33
+
34
+ spec.add_dependency 'clientele'
35
+
36
+ # ...
37
+
38
+ end
39
+ ```
40
+
41
+ Then install `clientele`.
42
+
43
+ ```bash
44
+ bundle install
45
+ ```
46
+
47
+ Finally, we can create our API Client class. We must configure it with a url to use as the base of our API.
48
+
49
+ ```ruby
50
+ # lib/example/api.rb
51
+
52
+ require 'clientele'
53
+
54
+ module Example
55
+ class API < Clientele::API
56
+
57
+ end
58
+ end
59
+ ```
60
+
61
+
62
+ ### Making Requests
63
+
64
+ Now that we have a client class, we can construct requests off of it to our API by providing the anatomy of an HTTP request.
65
+
66
+ #### Initializing a Client
67
+
68
+ Creating a client instance is as simple as passing in any configuration options into `new`. See the Configuration section for more options.
69
+
70
+ ```ruby
71
+ client = Example::API.new(root_url: 'http://example.com')
72
+ ```
73
+
74
+ Alternatively, you can use the `client` method to lazily initialize and access a global API client if you're not concerned about thread safety, or just experimenting with an API.
75
+
76
+ ```ruby
77
+ # Return client with default configuration
78
+ Example::API.client
79
+ #=> #<Example::API:0x007f85faa16468>
80
+
81
+ # Configure/reconfigure and return global client
82
+ Example::API.client(root_url: 'http://example.com')
83
+ #=> #<Example::API:0x007f85faa16468>
84
+ ```
85
+
86
+ Future calls to `client` will use this configured instance. Passing options into it will reconfigure this global client.
87
+
88
+ Finally, any unknown method calls on the client class attempt to see if the global client responds to them.
89
+
90
+ ```ruby
91
+ Example::API.foo
92
+ # will return the result of
93
+ Example::API.client.foo
94
+ # provided the client responds to foo.
95
+ ```
96
+
97
+ #### Building Requests on Client Instances
98
+
99
+ The simplest way to make requests is to call the HTTP verb you want on your client, passing in a hash with everything you need to in the request. For the rest of these examples we'll be using the global client.
100
+
101
+ ```ruby
102
+ request = Example::API.get(path: 'foo')
103
+ #=> #<struct Clientele::Request>
104
+ ```
105
+
106
+ Clients respond to any of the HTTP verbs found in `Clientele::Request::VERBS`. The resulting request can be triggered with `call` as if a Proc.
107
+
108
+ ```ruby
109
+ response = Example::API.get(path: 'foo').call
110
+ #=> #<Faraday::Response>
111
+ ```
112
+
113
+ Unlike other options, you can provide path as a direct argument rather than a keyword one.
114
+
115
+ ```ruby
116
+ Example::API.get(path: 'foo', query: {bar: :baz})
117
+ # is the same as
118
+ Example::API.get('foo', query: {bar: :baz})
119
+ ```
120
+
121
+ The options used to construct a request are:
122
+
123
+ Option | Default | Description
124
+ ------------------------------
125
+ path | `''` | The url path to build off of `root_url`.
126
+ query | `{}` | A hash of query string parameters.
127
+ body | `{}` | A hash representing the request payload.
128
+ headers | `client.configuration.default_headers` | A hash representing the request headers.
129
+ callback | `nil` | An optional callback `Proc` to pass the response into. If the request constructor receives a block, it will use that as a callback.
130
+
131
+ In actuality, these methods will instead return `Clientele::RequestBuilder` instances with your defined request inside. Regardless, the `call` method will invoke them the same.
132
+
133
+ Of course, all these features amount to at this point is a verbose HTTP Library. The power of `clientele` comes from using these components to define resources.
134
+
135
+
136
+ ### Creating Resources
137
+
138
+ Resources inherit from `Clientele::Resource` and map to namespaced endpoints of an API that deal with similar datatypes, as is found often in RESTful APIs.
139
+
140
+ ```ruby
141
+ # lib/example/api/resources.rb
142
+
143
+ require 'example/api/resources/foo`
144
+ ```
145
+
146
+ ```ruby
147
+ # lib/example/api/resources/foo.rb
148
+
149
+ module Example
150
+ class API < Clientele::API
151
+ module Resources
152
+ class Foo < Clientele::Resource
153
+
154
+
155
+
156
+ end
157
+ end
158
+ end
159
+ end
160
+ ```
161
+
162
+ ```ruby
163
+ # lib/example/api.rb
164
+
165
+ require 'lib/example/api/resources'
166
+
167
+ module Example
168
+ class API < Clientele::API
169
+
170
+ # ...
171
+
172
+ resource Resources::Foo
173
+
174
+ # ...
175
+
176
+ end
177
+ end
178
+ ```
179
+
180
+ Registering this resource on the client allows it to be invoked as a method.
181
+
182
+ ```ruby
183
+ Example::API.foos
184
+ #=> #<Clientele::RequestBuilder>
185
+ ```
186
+
187
+ Calling this request will send a `GET` request to `http://example.com/foos' with default headers and no query string parameters.
188
+
189
+ Using the request builder API, we can define class methods on the resource that accomplish any HTTP request. If we provide a path it will be appended to the resource's path (ie. `'foo/path'`); otherwise it will send the request to the resource root. For instance, to get an ActiveRecord-inspired request DSL:
190
+
191
+ ```ruby
192
+ # lib/example/api/resources/foo.rb
193
+
194
+ module Example
195
+ class API < Clientele::API
196
+ module Resources
197
+ class Foo < Clientele::Resource
198
+
199
+ class << self
200
+
201
+ def all(&callback)
202
+ get &callback
203
+ end
204
+
205
+ def where(query={}, &callback)
206
+ get query: query, &callback
207
+ end
208
+
209
+ def create(body={}, &callback)
210
+ post body: body, &callback
211
+ end
212
+
213
+ def fetch(id, query={}, &callback)
214
+ get id, query: query, &callback
215
+ end
216
+
217
+ def update(id, body={}, &callback)
218
+ patch id, body: body, &callback
219
+ end
220
+
221
+ def destroy(id, &callback)
222
+ delete id, &callback
223
+ end
224
+
225
+ end
226
+
227
+ end
228
+ end
229
+ end
230
+ end
231
+ ```
232
+
233
+
234
+ ### Using Resources
235
+
236
+ Introduction
237
+
238
+ #### Making Requests to Resources
239
+
240
+ #### Making Asyncronous Requests
241
+
242
+ #### Chaining Resource Requests
243
+
244
+ #### Iterating Across Paginated Resources
245
+
246
+
247
+ ### Configuration
248
+
249
+ `Clientele::API` instances each have their own configuration that they receive from a class-level configuration object. Both the class level and instance level configurations can be customized on demand by you, the API client developer, or consumers of your client.
250
+
251
+ #### Configuration Options
252
+
253
+ Option | Default | Description
254
+ ------------------------------
255
+ root_url | Required | The root url against which all requests are made.
256
+ logger | `Logger.new($stdout)` | The logger for `clientele` to use.
257
+ adapter | `Faraday.default_adapter` | The `faraday` adapter to use.
258
+ headers | `{}` | Headers to use with every request.
259
+ hashify_content_type | /\bjson$/ | A regex `faraday` applies to response content types to determine whether or not to try to convert the payload into a hash.
260
+ follow_redirects | `true` | Whether or not to follow redirects.
261
+ redirect_limit | `5` | How deep to follow redirects.
262
+
263
+ #### Default Library Configuration
264
+
265
+ To override `clientele`'s default options within your library, use the `configure` class method on your API client. You'll probably want to do this for at least the `root_url` option since it has no default.
266
+
267
+ ```ruby
268
+ #lib/example/api.rb
269
+ require 'clientele'
270
+
271
+ module Example
272
+ class API < Clientele::API
273
+
274
+ configure do |config|
275
+
276
+ # Required options
277
+ config.root_url = "http://example.com"
278
+
279
+ # Optional overrides
280
+ config.headers = {
281
+ 'Accept' => 'application/json',
282
+ 'Content-Type' => 'application/json',
283
+ }
284
+ # must add 'net-http-persistent' to gemspec to use:
285
+ config.adapter = :net_http_peristent
286
+
287
+ # Custom configuration values
288
+ config.custom = :foobar
289
+
290
+ end
291
+
292
+ end
293
+ end
294
+ ```
295
+
296
+ You'll probably want to include the table above, alongside any modifications or additions, in your client's documentation.
297
+
298
+ #### Default User Configuration
299
+
300
+ Users of your API Client can also access the class level `configure` method to change default configuration options within their project. This should be done early on in the script, library loading stage, or boot process so it can take effect before any clients are instanciated.
301
+
302
+ Rails users would put this in an initializer.
303
+
304
+ ```ruby
305
+ #my_app/config/initializers/example-api.rb
306
+ require 'example/api'
307
+
308
+ Example::API.configure do |config|
309
+ config.root_url = 'http://dev.example.com'
310
+ end
311
+ ```
312
+
313
+ You may also wish to document how to do this in your gem.
314
+
315
+
316
+
317
+ Contributing
318
+ ------------
25
319
 
26
320
  1. Fork it ( https://github.com/[my-github-username]/clientele/fork )
27
321
  2. Create your feature branch (`git checkout -b my-new-feature`)
data/Rakefile CHANGED
@@ -1,2 +1,16 @@
1
1
  require "bundler/gem_tasks"
2
2
 
3
+ desc "Open a pry console preloaded with this library"
4
+ task console: 'console:pry'
5
+
6
+ namespace :console do
7
+
8
+ task :pry do
9
+ sh "bundle exec pry -I lib -r clientele.rb"
10
+ end
11
+
12
+ task :irb do
13
+ sh "bundle exec irb -I lib -r clientele.rb"
14
+ end
15
+
16
+ end
data/lib/clientele/api.rb CHANGED
@@ -16,7 +16,6 @@ module Clientele
16
16
 
17
17
  extend SingleForwardable
18
18
  def_delegator :configuration, :logger
19
- def_delegators :request, *Request::VERBS
20
19
 
21
20
  class_attribute :resources, instance_predicate: false
22
21
  self.resources = {}
@@ -25,7 +24,13 @@ module Clientele
25
24
 
26
25
  def client(opts={})
27
26
  autoconfigure!
28
- @client ||= new(opts)
27
+ if @client
28
+ @client.tap do |client|
29
+ client.configuration.load_hash(opts)
30
+ end
31
+ else
32
+ @client = new(opts)
33
+ end
29
34
  end
30
35
 
31
36
  def logger
@@ -34,7 +39,7 @@ module Clientele
34
39
  end
35
40
 
36
41
  def resource(klass)
37
- self.resources = resources.merge(:"#{klass}" => klass)
42
+ self.resources = resources.merge(klass.method_name.to_sym => klass)
38
43
  end
39
44
 
40
45
  private
@@ -63,21 +68,17 @@ module Clientele
63
68
  self.configuration.load_hash opts
64
69
  end
65
70
 
66
- protected
67
-
68
- def request
69
- Request
70
- end
71
-
72
71
  private
73
72
 
74
73
  def respond_to_missing?(method_name, include_private=false)
75
- resources.keys.include?(method_name) or super
74
+ resources.keys.include?(method_name) or Request::VERBS.include?(method_name) or super
76
75
  end
77
76
 
78
77
  def method_missing(method_name, *args, &block)
79
78
  if resources.keys.include? method_name
80
- RequestBuilder.new(self.class::resources[method_name], client: self)
79
+ RequestBuilder.new(resources[method_name], client: self)
80
+ elsif Request::VERBS.include? method_name
81
+ RequestBuilder.new(Request.send(method_name, *args), client: self)
81
82
  else; super; end
82
83
  end
83
84
 
@@ -5,13 +5,15 @@ require 'faraday'
5
5
  module Clientele
6
6
  class Configuration < BlockParty::Configuration
7
7
 
8
- attr_accessor :logger, :adapter, :headers, :hashify_content_type, :root_url
8
+ attr_accessor :logger, :adapter, :headers, :hashify_content_type, :root_url, :follow_redirects, :redirect_limit
9
9
 
10
10
  def initialize
11
11
  self.logger = Logger.new($stdout)
12
12
  self.adapter = Faraday.default_adapter
13
13
  self.headers = {}
14
14
  self.hashify_content_type = /\bjson$/
15
+ self.follow_redirects = true
16
+ self.redirect_limit = 5
15
17
  end
16
18
 
17
19
  end
@@ -12,8 +12,14 @@ module Clientele
12
12
 
13
13
  VERBS = %i[get post put patch delete]
14
14
  VERBS.each do |verb|
15
- define_singleton_method verb do |opts = {}, &callback|
16
- new opts.merge(verb: __method__, callback: callback)
15
+ define_singleton_method verb do |path = '', opts = {}, &callback|
16
+ new(
17
+ opts.tap do |opts|
18
+ opts.merge!(verb: __method__)
19
+ # opts.merge!(path: path) unless opts[:path]
20
+ # opts.merge!(callback: callback) if callback
21
+ end
22
+ )
17
23
  end
18
24
  end
19
25
 
@@ -39,7 +45,6 @@ module Clientele
39
45
  def call
40
46
  callback ? callback.call(response) : response
41
47
  end
42
- alias_method :~, :call
43
48
 
44
49
  def + other
45
50
  self.class.new(
@@ -66,8 +71,15 @@ module Clientele
66
71
 
67
72
  def faraday_client
68
73
  Faraday.new(options[:root_url]) do |conn|
74
+
75
+ conn.use FaradayMiddleware::FollowRedirects, limit: options[:redirect_limit] if options[:follow_redirects]
76
+
77
+ conn.request :url_encoded
78
+
69
79
  conn.response :rashify
80
+ conn.response :logger
70
81
  conn.response :json, content_type: options[:hashify_content_type], preserve_raw: true
82
+
71
83
  conn.adapter options[:adapter]
72
84
  end
73
85
  end
@@ -76,7 +88,7 @@ module Clientele
76
88
  def defaults
77
89
  {
78
90
  verb: :get,
79
- path: nil,
91
+ path: '',
80
92
  query: {},
81
93
  body: {},
82
94
  headers: {},
@@ -9,14 +9,13 @@ module Clientele
9
9
  alias_method :to_a, :stack
10
10
 
11
11
  def initialize(*request_components, client: API.client)
12
- @stack = Array(request_components).flatten
12
+ @stack = request_components.flatten
13
13
  @client = client
14
14
  end
15
15
 
16
16
  def call
17
17
  build.call
18
18
  end
19
- alias_method :~, :call
20
19
 
21
20
  protected
22
21
 
@@ -49,15 +48,15 @@ module Clientele
49
48
  end
50
49
  end
51
50
 
52
- def to_s
53
- merge_paths(stack.map(&:to_s))
51
+ def path
52
+ merge_paths(stack.map(&:path))
54
53
  end
55
54
 
56
55
  private
57
56
 
58
57
  def method_missing(method_name, *args, &block)
59
- if API::resources.keys.include? method_name
60
- tap { |builder| builder.stack << API::resources[method_name] }
58
+ if client.resources.keys.include? method_name
59
+ tap { |builder| builder.stack << client.resources[method_name] }
61
60
  elsif stack.last.respond_to? :each_with_builder and method_name == :each
62
61
  stack.last.each_with_builder(self, &block)
63
62
  elsif stack.last.respond_to? method_name, false
@@ -22,11 +22,12 @@ module Clientele
22
22
  Proc.new do
23
23
 
24
24
  def next_page(request)
25
- request.query[:page] += 1
25
+ request.query[:page] ||= 0
26
+ request.query[:page] += 1
26
27
  end
27
28
 
28
29
  def total(response)
29
- response.headers.fetch('x-total-count', Float::INFINITY)
30
+ response.headers.fetch('x-total-count', Float::INFINITY).to_f
30
31
  end
31
32
 
32
33
  def pages(response)
@@ -12,33 +12,42 @@ module Clientele
12
12
 
13
13
  class << self
14
14
  include Clientele::Utils
15
+
15
16
  attr_reader :subclasses
16
- attr_accessor :path
17
-
18
- def request(verb, path='', query: {}, body: {}, options: {}, &callback)
19
- Request.send(verb,
20
- path: merge_paths(@path || to_s, path),
21
- query: query,
22
- body: body,
23
- options: options,
24
- resource: self,
25
- &callback
26
- )
17
+
18
+ Request::VERBS.each do |verb|
19
+ define_method verb do |path_segment = '', opts = {}, &callback|
20
+ path_segment, opts = opts[:path].to_s, path_segment if path_segment.is_a? Hash
21
+ Request.new(opts.merge(path: merge_paths(path, path_segment || opts[:path].to_s)), &callback)
22
+ end
23
+ end
24
+
25
+ def request(verb, path = '', opts = {}, &callback)
26
+ send verb, path, opts, &callback
27
27
  end
28
28
 
29
29
  def to_request(options={}, &callback)
30
- request :get, options: options, callback: callback
30
+ get options: options, &callback
31
31
  end
32
32
 
33
- def to_s
33
+ def default_path
34
34
  self.name.split('::').last.pluralize.underscore
35
35
  end
36
36
 
37
+ def path
38
+ @path || default_path
39
+ end
40
+
41
+ def method_name
42
+ @method_name || path
43
+ end
44
+
37
45
  private
38
46
 
39
47
  def inherited(base)
40
48
  @subclasses << base
41
49
  end
50
+
42
51
  end
43
52
 
44
53
  end
@@ -1,3 +1,3 @@
1
1
  module Clientele
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clientele
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Keele
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-03 00:00:00.000000000 Z
11
+ date: 2014-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -187,4 +187,3 @@ signing_key:
187
187
  specification_version: 4
188
188
  summary: DSL for creating RESTful API clients for external services.
189
189
  test_files: []
190
- has_rdoc: