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 +115 -47
- data/api_bee.gemspec +3 -3
- data/examples/github_api.rb +6 -6
- data/lib/api_bee/adapters/hash.rb +1 -2
- data/lib/api_bee/node.rb +78 -1
- data/lib/api_bee/version.rb +1 -1
- data/lib/api_bee.rb +48 -8
- data/spec/setup_spec.rb +41 -1
- metadata +5 -5
data/README.mkd
CHANGED
@@ -51,85 +51,153 @@ class ApiBee::Adapters::Special
|
|
51
51
|
end
|
52
52
|
```
|
53
53
|
|
54
|
-
|
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
|
-
|
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
|
-
##
|
70
|
+
## Lazy-loading
|
65
71
|
|
66
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
74
|
+
# JSON Dataset
|
75
|
+
{
|
76
|
+
'user': {
|
77
|
+
'name': 'Ismael',
|
78
|
+
'href': 'http://api.com/users/ismael'
|
79
|
+
}
|
80
|
+
}
|
71
81
|
|
72
|
-
|
73
|
-
|
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
|
-
|
76
|
-
api.post('/products', :title => 'Foo', :price => 100.0)
|
91
|
+
This works for objects in collections too.
|
77
92
|
|
78
|
-
|
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
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
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
|
-
##
|
141
|
+
## Adapter-wide configuration
|
97
142
|
|
98
|
-
|
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
|
-
|
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.
|
165
|
+
api = ApiBee.setup(MyCustomAdapter) do |config|
|
166
|
+
config.expose :delete, :post
|
167
|
+
end
|
118
168
|
|
119
|
-
|
169
|
+
# This still wraps your adapter's get() method and adds lazy-loading and pagination
|
170
|
+
api.get('/products').first[:title]
|
120
171
|
|
121
|
-
|
172
|
+
# This delegates directly to MyCustomAdapter#post()
|
173
|
+
api.post('/products', :title => 'Foo', :price => 100.0)
|
174
|
+
```
|
122
175
|
|
123
|
-
|
176
|
+
## finding a single resource
|
124
177
|
|
125
|
-
|
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.
|
13
|
-
s.
|
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
|
|
data/examples/github_api.rb
CHANGED
@@ -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
|
-
|
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)
|
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
|
-
|
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)
|
data/lib/api_bee/version.rb
CHANGED
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
|
-
|
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)
|
13
|
-
|
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
|
-
|
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.
|
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-
|
13
|
+
date: 2011-09-26 00:00:00 Z
|
14
14
|
dependencies: []
|
15
15
|
|
16
|
-
description:
|
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:
|
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
|