api_bee 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.mkd CHANGED
@@ -51,85 +51,153 @@ class ApiBee::Adapters::Special
51
51
  end
52
52
  ```
53
53
 
54
- The use it:
54
+ ApiBee wraps your adapter and makes results lazily-loaded. That is, actual requests won't be made until accessing attributes or iterating the data set.
55
+
56
+ Use it:
55
57
 
56
58
  ```ruby
57
59
  api = ApiBee.setup :special, optional_custom_data
58
60
 
59
- api.get('/my/resources').each do |r|
61
+ # No actual request made
62
+ resources = api.get('/my/resources')
63
+
64
+ # Requests once under the hood so you can iterate
65
+ resources.each do |r|
60
66
  r[:name]
61
67
  end
62
68
  ```
63
69
 
64
- ## Delegate to adapter
70
+ ## Lazy-loading
65
71
 
66
- Lazy-loading and paginating resources is great for GET requests, but you might want to still use your adapter's other methods.
72
+ If an object in a response has a 'href' attribute, it will be used to fetch more data if you ask for an attribute currently not in the object.
67
73
 
68
- api = ApiBee.setup(MyCustomAdapter) do |config|
69
- config.expose :delete, :post
70
- end
74
+ # JSON Dataset
75
+ {
76
+ 'user': {
77
+ 'name': 'Ismael',
78
+ 'href': 'http://api.com/users/ismael'
79
+ }
80
+ }
71
81
 
72
- # This still wraps your adapter's get() method and adds lazy-loading and pagination
73
- api.get('/products').first[:title]
82
+ ```ruby
83
+ # Instantiate object
84
+ user = api.get('/user')
85
+ # No extra request made
86
+ user[:name]
87
+ # Extra request to http://api.com/users/ismael to fetch more user data
88
+ user[:last_name]
89
+ ```
74
90
 
75
- # This delegates directory to MyCustomAdapter#post()
76
- api.post('/products', :title => 'Foo', :price => 100.0)
91
+ This works for objects in collections too.
77
92
 
78
- ## finding a single resource
93
+ # JSON collection
94
+ {
95
+ 'users': {
96
+ 'total_entries': 100,
97
+ 'page': 1,
98
+ 'per_page': 2,
99
+ 'href': 'http://api.com/users',
100
+ 'entries': [
101
+ {
102
+ 'name': 'Ismael',
103
+ 'href': 'http://api.com/users/ismael'
104
+ },
105
+ {
106
+ 'name': 'John',
107
+ 'href': 'http://api.com/users/john'
108
+ }
109
+ ]
110
+ }
111
+ }
79
112
 
80
- There's a special get_one method that you can call on lists. It delegates to the adapter and it's useful for finding a single resource in the context of a paginated list.
113
+
114
+ ```ruby
115
+ # Instantiate collection
116
+ users = api.get('/users') # no request yet
117
+
118
+ users.total_entries # => 100 # request made
119
+ users.size # => 2, current page
120
+ users.each ... # iterate current page
121
+ users.current_page # => 1
122
+ users.has_next_page? # => true
123
+ users.next_page # => 2
124
+
125
+ # Access entry. No request made
126
+ ismael = users.first
127
+ ismael[:name] # => 'ismael'
128
+ ismael[:last_name'] #=> request made to http://api.com/users/ismael
129
+ ```
81
130
 
82
- resources = api.get('/my/resources')
83
- resource = resources.get_one('foobar')
84
-
85
- That delegates to Adapter#get_one passing 2 arguments: the list's href and the passed name or identifier, so:
131
+ ## Per instance configuration
132
+
133
+ You can configure some variables on a per-instance basis. To configure the attribute name used to access a resource's URL (defaults to :href):
86
134
 
87
135
  ```ruby
88
- class ApiBee::Adapters::Special
89
- # ...
90
- def get_one(href, id)
91
- get "#{href}/#{id}"
92
- end
136
+ api = ApiBee.setup(MyCustomAdapter) do |config|
137
+ config.uri_property_name = :uri # use :uri instead
93
138
  end
94
139
  ```
95
140
 
96
- ## Lazy loading
141
+ ## Adapter-wide configuration
97
142
 
98
- ApiBee wraps your adapters in lazy-loading objects. API calls will only be issued when accessing or iterating data.
143
+ You can declare configuration in the adapter definition itself, too. Just define the config_api_bee class method in your adapter:
99
144
 
100
- The 'href' property in entities will be used to load more data. For example:
145
+ ```ruby
146
+ class MyCustomAdapter
147
+
148
+ def get(path, options)
149
+ # ...
150
+ end
151
+
152
+ def self.config_api_bee(config)
153
+ config.uri_property_name = :uri
154
+ end
155
+ end
156
+
157
+ api = ApiBee.setup(MyCustomAdapter) # instance is configured correctly
158
+ ```
159
+
160
+ ## Delegate to adapter
161
+
162
+ Lazy-loading and paginating resources is great for GET requests, but you might want to still use your adapter's other methods.
101
163
 
102
- # /resources
103
- [
104
- {
105
- 'title': 'Foo bar',
106
- 'href': '/resources/foo-bar'
107
- }
108
- ]
109
-
110
- # /resources/foo-bar
111
- {
112
- 'title': 'Foo bar',
113
- 'description': 'Foo description'
114
- }
115
-
116
164
  ```ruby
117
- api = ApiBee.get(:some_adapter)
165
+ api = ApiBee.setup(MyCustomAdapter) do |config|
166
+ config.expose :delete, :post
167
+ end
118
168
 
119
- resources = api.get('/resources') # no API call is made
169
+ # This still wraps your adapter's get() method and adds lazy-loading and pagination
170
+ api.get('/products').first[:title]
120
171
 
121
- resource = resources.first # call to /resources is made
172
+ # This delegates directly to MyCustomAdapter#post()
173
+ api.post('/products', :title => 'Foo', :price => 100.0)
174
+ ```
122
175
 
123
- resource['title'] # => 'Foo bar', title data available
176
+ ## finding a single resource
124
177
 
125
- resource['description'] # => 'Foo description'. Makes internal new request to /resources/foo-bar
178
+ There's a special get_one method that you can call on lists. It delegates to the adapter and it's useful for finding a single resource in the context of a paginated list.
179
+
180
+ ```ruby
181
+ resources = api.get('/my/resources')
182
+ resource = resources.get_one('foobar')
126
183
  ```
127
-
128
-
184
+
185
+ That delegates to Adapter#get_one passing 2 arguments: the list's href and the passed name or identifier, so:
186
+
187
+ ```ruby
188
+ class ApiBee::Adapters::Special
189
+ # ...
190
+ def get_one(href, id)
191
+ get "#{href}/#{id}"
192
+ end
193
+ end
194
+ ```
195
+
129
196
  ## Hash adapter
130
197
 
131
198
  ApiBee ships with an in-memory Hash adapter so it can be use with test/local data (for example a YAML file).
132
199
 
200
+
133
201
  ```ruby
134
202
  api = ApiBee.setup(:hash, YAML.load_file('./my_data.yml'))
135
203
 
@@ -152,4 +220,4 @@ products.prev_page # => 1
152
220
 
153
221
  ## Examples
154
222
 
155
- See examples/github_api.rb for an adapter that paginates Github's API by decorating it's results with ApiBee's required pagination properties
223
+ See [examples/github_api.rb](https://github.com/ismasan/ApiBee/blob/master/examples/github_api.rb) for an adapter that paginates Github's API by decorating it's results with ApiBee's required pagination properties
data/api_bee.gemspec CHANGED
@@ -8,9 +8,9 @@ Gem::Specification.new do |s|
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Ismael Celis"]
10
10
  s.email = ["ismaelct@gmail.com"]
11
- s.homepage = ""
12
- s.summary = %q{Small Ruby client for discoverable, paginated JSON APIs}
13
- s.description = %q{API Bee is a small client / spec for a particular style of JSON API. USe Hash adapter for local data access.}
11
+ s.homepage = "https://github.com/ismasan/ApiBee"
12
+ s.description = %q{Small Ruby client for discoverable, lazily-loaded, paginated JSON APIs}
13
+ s.summary = %q{API Bee is a small client / spec for a particular style of JSON API. Use Hash adapter for local data access.}
14
14
 
15
15
  s.rubyforge_project = "api_bee"
16
16
 
@@ -9,6 +9,10 @@ require 'json'
9
9
  #
10
10
  class GithubAdapter
11
11
 
12
+ def self.config_api_bee(config)
13
+ config.uri_property_name = :url
14
+ end
15
+
12
16
  def initialize
13
17
  @url = URI.parse('https://api.github.com')
14
18
  @http = Net::HTTP.new(@url.host, @url.port)
@@ -27,14 +31,12 @@ class GithubAdapter
27
31
 
28
32
  if response.kind_of?(Net::HTTPOK)
29
33
  results = JSON.parse response.body
30
- results = if results.is_a?(Array)
34
+ if results.is_a?(Array)
31
35
  # decorate returned array so it complies with APiBee's pagination params
32
36
  paginate results, options, path, response
33
37
  else
34
38
  results
35
39
  end
36
-
37
- results
38
40
  else
39
41
  nil
40
42
  end
@@ -80,9 +82,7 @@ end
80
82
 
81
83
  ## Instantiate your wrapped API
82
84
 
83
- api = ApiBee.setup(GithubAdapter) do |config|
84
- config.uri_property_name = :url
85
- end
85
+ api = ApiBee.setup(GithubAdapter)
86
86
 
87
87
  repos = api.get('/users/ismasan/repos')
88
88
 
@@ -12,7 +12,7 @@ module ApiBee
12
12
 
13
13
  def get(href, opts = {})
14
14
  segments = parse_href(href)
15
- found = segments.inject(data) do |mem,i|
15
+ segments.inject(data) do |mem,i|
16
16
  case mem
17
17
  when ::Hash
18
18
  handle_hash_data mem, i, opts
@@ -22,7 +22,6 @@ module ApiBee
22
22
  mem
23
23
  end
24
24
  end
25
- found
26
25
  end
27
26
 
28
27
  def get_one(href, id)
data/lib/api_bee/node.rb CHANGED
@@ -1,5 +1,11 @@
1
1
  module ApiBee
2
2
 
3
+ # == API response objects
4
+ #
5
+ # A node wraps Hash data returned by adapter.get(path)
6
+ # It inspects the returned data and tries to add lazy-loading of missing attributes (provided there is an :href attribute)
7
+ # and pagination (see Node::List)
8
+ #
3
9
  class Node
4
10
 
5
11
  def self.simbolized(hash)
@@ -9,6 +15,19 @@ module ApiBee
9
15
  end
10
16
  end
11
17
 
18
+ # Factory. Inspect passed attribute hash for pagination fields and reurns one of Node::List for paginated node lists
19
+ # or Node::Single for single nodes
20
+ # A node (list or single) may contain nested lists or single nodes. This is handled transparently by calling Node.resolve
21
+ # when accessing nested attributes of a node.
22
+ #
23
+ # Example:
24
+ #
25
+ # node = Node.resolve(an_adapter, config_object, {:total_entries => 10, :href => '/products', :entries => [...]})
26
+ # # node is a Node::List because is has pagination fields
27
+ #
28
+ # node = Node.resolve(an_adapter, config_object, {:name => 'Ismael', :bday => '11/29/77'})
29
+ # # node is a Node::Single because it doesn't represent a paginated list
30
+ #
12
31
  def self.resolve(adapter, config, attrs, href = nil)
13
32
  attrs = simbolized(attrs)
14
33
  keys = attrs.keys.map{|k| k.to_sym}
@@ -33,6 +52,24 @@ module ApiBee
33
52
  @attributes
34
53
  end
35
54
 
55
+ # Lazy loading attribute accessor.
56
+ # Attempts to look for an attribute in this node's present attributes
57
+ # If the attribute is missing and the node has a :href attribute pointing to more data for this resource
58
+ # it will delegate to the adapter for more data, update it's attributes and return the found value, if any
59
+ #
60
+ # Example:
61
+ #
62
+ # data = {
63
+ # :href => '/products/6',
64
+ # :title => 'Ipod'
65
+ # }
66
+ #
67
+ # node = Node.resolve(adapter, config, data)
68
+ #
69
+ # node[:title] # => 'Ipod'.
70
+ #
71
+ # node[:price] # new request to /products/6
72
+ #
36
73
  def [](attribute_name)
37
74
  if respond_to?(attribute_name)
38
75
  send attribute_name
@@ -73,14 +110,54 @@ module ApiBee
73
110
  @complete = true
74
111
  end
75
112
 
113
+ # == Single node
114
+ #
115
+ # Resolved when initial data hash doesn't include pagination attributes
116
+ #
76
117
  class Single < Node
77
118
 
78
119
  end
79
-
120
+
121
+ # == Paginated node list
122
+ #
123
+ # Resolved by Node.resolve when initial data hash contains pagination attributes :total_entries, :href and :entries
124
+ # Note that these are the default values for those fields and they can be configured per-API via the passed config object.
125
+ #
126
+ # A Node::List exposes methods useful for paginating a list of nodes.
127
+ #
128
+ # Example:
129
+ #
130
+ # data = {
131
+ # :total_entries => 10,
132
+ # :href => '/products',
133
+ # :page => 1,
134
+ # :per_page => 20,
135
+ # :entries => [
136
+ # {
137
+ # :title => 'Ipod'
138
+ # },
139
+ # {
140
+ # :title => 'Ipad'
141
+ # },
142
+ # ...
143
+ # ]
144
+ # }
145
+ #
146
+ # list = Node.resolve(adapter, config, data)
147
+ # list.current_page # => 1
148
+ # list.has_next_page? # => true
149
+ # list.next_page # => 2
150
+ # list.size # => 20
151
+ # list.each ... # iterate current page
152
+ # list.first #=> an instance of Node::Single
153
+ #
80
154
  class List < Node
81
155
 
82
156
  DEFAULT_PER_PAGE = 100
83
157
 
158
+ # Get one resource from this list
159
+ # Delegates to adapter.get_one(href, id) and resolves result.
160
+ #
84
161
  def get_one(id)
85
162
  data = @adapter.get_one(@href, id)
86
163
  data.nil? ? nil : Node.resolve(@adapter, @config, data, @href)
@@ -1,3 +1,3 @@
1
1
  module ApiBee
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/lib/api_bee.rb CHANGED
@@ -2,22 +2,52 @@ require 'ostruct'
2
2
  module ApiBee
3
3
 
4
4
  class << ApiBee
5
- attr_reader :config
6
5
 
6
+ # Setup and instantiates a new API proxy (ApiBee::Proxy)
7
+ # by wrapping a bundled or custom API adapter and passing optional arguments
8
+ #
9
+ # When a hash is passed as the adapter class, it will look for that class file in
10
+ # the bundled adapters directory:
11
+ #
12
+ # ApiBee.setup(:hash, {})
13
+ #
14
+ # Looks for ApiBee::Adapters::Hash in lib/api_bee/adapters
15
+ #
16
+ # You can pass a custom adapter class
17
+ #
18
+ # Example:
19
+ #
20
+ # class MyAdapter
21
+ # def initialize(api_key)
22
+ # @url = 'http://myservice.com'
23
+ # end
24
+ #
25
+ # def get(path, options = {})
26
+ # # Fetch data from your service here
27
+ # end
28
+ # end
29
+ #
30
+ # api = ApiBee.setup(MyAdapter, 'MY_API_KEY')
31
+ #
32
+ # That gives you an instance of ApiBee::Proxy wrapping an instance of your MyAdapter initialized with your key.
33
+ #
7
34
  def setup(adapter_klass, *args)
8
35
 
9
- adapter = if adapter_klass.is_a?(::Symbol)
36
+ adapter_klass = (
10
37
  require File.join('api_bee', 'adapters', adapter_klass.to_s)
11
38
  klass = adapter_klass.to_s.gsub(/(^.{1})/){$1.upcase}
12
- Adapters.const_get(klass).new(*args)
13
- else
14
- adapter_klass.new *args
15
- end
16
-
17
- raise NoMethodError, "Adapter must implement #get(path, *args) method" unless adapter.respond_to?(:get)
39
+ Adapters.const_get(klass)
40
+ ) if adapter_klass.is_a?(Symbol)
18
41
 
19
42
  config = new_config
43
+ # If adapter-wide config method
44
+ adapter_klass.config_api_bee(config) if adapter_klass.respond_to?(:config_api_bee)
45
+ # If config block passed per api instance
20
46
  yield config if block_given?
47
+
48
+ adapter = adapter_klass.new(*args)
49
+ raise NoMethodError, "Adapter must implement #get(path, *args) method" unless adapter.respond_to?(:get)
50
+
21
51
  Proxy.new adapter, config
22
52
  end
23
53
 
@@ -38,6 +68,16 @@ module ApiBee
38
68
 
39
69
  class Config < OpenStruct
40
70
 
71
+ # Delegate method calls to your wrapped adapter
72
+ #
73
+ # Example:
74
+ #
75
+ # api = ApiBee.setup(TwitterAdapter) do |config|
76
+ # config.expose :post_message, :delete_message
77
+ # end
78
+ #
79
+ # Now calls to api.post_message and api.delete_message will be delegated to an instance of TwitterAdapter
80
+ #
41
81
  def expose(*fields)
42
82
  self.adapter_delegators = fields
43
83
  end
data/spec/setup_spec.rb CHANGED
@@ -41,7 +41,47 @@ describe 'ApiBee.setup' do
41
41
 
42
42
  end
43
43
 
44
- context 'configuration' do
44
+ describe 'global adapter configuration' do
45
+
46
+ before do
47
+ require 'api_bee/adapters/hash'
48
+ @adapter_klass = Class.new(ApiBee::Adapters::Hash) do
49
+
50
+ def self.config_api_bee(config)
51
+ config.expose :fetch
52
+ config.uri_property_name = :uri
53
+ end
54
+
55
+ def fetch(*args)
56
+ @data.fetch *args
57
+ end
58
+
59
+ end
60
+
61
+ @api = ApiBee.setup(@adapter_klass, {
62
+ :user => {
63
+ :name => 'ismael 1',
64
+ :uri => '/users/ismael1'
65
+ }
66
+ })
67
+ end
68
+
69
+ it 'should have configure defaults for all instances of adapter' do
70
+ user = @api.get('/user')
71
+ user[:name].should == 'ismael 1'
72
+ @api.adapter.should_receive(:get).with('/users/ismael1').and_return(:last_name => 'Celis')
73
+
74
+ user[:last_name].should == 'Celis'
75
+ end
76
+
77
+ it 'should delegate configured methods to adapter' do
78
+ @api.fetch(:user, 'foo').should == {:name => 'ismael 1', :uri => '/users/ismael1'}
79
+ @api.fetch(:blah, 'foo').should == 'foo'
80
+ end
81
+
82
+ end
83
+
84
+ context 'per-instance configuration' do
45
85
 
46
86
  before do
47
87
  @api1 = ApiBee.setup(:hash, {
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: api_bee
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.3
5
+ version: 0.0.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Ismael Celis
@@ -10,10 +10,10 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-09-22 00:00:00 Z
13
+ date: 2011-09-26 00:00:00 Z
14
14
  dependencies: []
15
15
 
16
- description: API Bee is a small client / spec for a particular style of JSON API. USe Hash adapter for local data access.
16
+ description: Small Ruby client for discoverable, lazily-loaded, paginated JSON APIs
17
17
  email:
18
18
  - ismaelct@gmail.com
19
19
  executables: []
@@ -41,7 +41,7 @@ files:
41
41
  - spec/node_spec.rb
42
42
  - spec/setup_spec.rb
43
43
  - spec/spec_helper.rb
44
- homepage: ""
44
+ homepage: https://github.com/ismasan/ApiBee
45
45
  licenses: []
46
46
 
47
47
  post_install_message:
@@ -67,7 +67,7 @@ rubyforge_project: api_bee
67
67
  rubygems_version: 1.8.6
68
68
  signing_key:
69
69
  specification_version: 3
70
- summary: Small Ruby client for discoverable, paginated JSON APIs
70
+ summary: API Bee is a small client / spec for a particular style of JSON API. Use Hash adapter for local data access.
71
71
  test_files:
72
72
  - spec/hash_adapter_spec.rb
73
73
  - spec/node_spec.rb