api_bee 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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