api_bee 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.mkd CHANGED
@@ -61,6 +61,24 @@ api.get('/my/resources').each do |r|
61
61
  end
62
62
  ```
63
63
 
64
+ ## finding a single resource
65
+
66
+ There's a special find_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.
67
+
68
+ resources = api.get('/my/resources')
69
+ resource = resources.find_one('foobar')
70
+
71
+ That delegates to Adapter#find_one passing 2 arguments: the list's href and the passed name or identifier, so:
72
+
73
+ ```ruby
74
+ class ApiBee::Adapters::Special
75
+ # ...
76
+ def find_one(href, id)
77
+ get "#{href}/#{id}"
78
+ end
79
+ end
80
+ ```
81
+
64
82
  ## Lazy loading
65
83
 
66
84
  ApiBee wraps your adapters in lazy-loading objects. API calls will only be issued when accessing or iterating data.
@@ -117,3 +135,7 @@ products.has_prev_page? # => true
117
135
 
118
136
  products.prev_page # => 1
119
137
  ```
138
+
139
+ ## Examples
140
+
141
+ See examples/github_api.rb for an adapter that paginates Github's API by decorating it's results with ApiBee's required pagination properties
@@ -0,0 +1,104 @@
1
+ $LOAD_PATH.unshift '../lib'
2
+ require 'api_bee'
3
+ #require 'net/http'
4
+ require 'net/https'
5
+ require 'uri'
6
+ require 'json'
7
+
8
+ class GithubAdapter
9
+
10
+ def initialize
11
+ ApiBee.config.uri_property_name = :url
12
+ @url = URI.parse('https://api.github.com')
13
+ @http = Net::HTTP.new(@url.host, @url.port)
14
+ @http.use_ssl = true
15
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
16
+ end
17
+
18
+ def get(path, options = {})
19
+ per_page = (options[:per_page] || 20).to_i
20
+ page = (options[:page] || 1).to_i
21
+
22
+ q = options.map{|k,v| "#{k}=#{v}"}.join('&')
23
+
24
+ fullpath = "#{path}?#{q}"
25
+
26
+ request = Net::HTTP::Get.new(fullpath)
27
+ response = @http.request(request)
28
+
29
+ if response.kind_of?(Net::HTTPOK)
30
+ results = JSON.parse response.body
31
+ if results.is_a?(Array)
32
+ # decorate returned array so it complies with APiBee's pagination params
33
+ results = {
34
+ :entries => results,
35
+ :page => page,
36
+ :per_page => per_page,
37
+ :url => path
38
+ }
39
+ # Extract last page number and entries count. Github uses a 'Link' header
40
+ if link = response["link"]
41
+ last_page = extract_last_page(link) || page # if no 'last' link, we're on the last page
42
+ results.update(
43
+ :total_entries => last_page.to_i * per_page
44
+ )
45
+ end
46
+ else
47
+ results
48
+ end
49
+
50
+ results
51
+ else
52
+ nil
53
+ end
54
+
55
+ end
56
+
57
+ def find_one(href, id)
58
+ get id
59
+ end
60
+
61
+ protected
62
+
63
+ def extract_last_page(link)
64
+ aa = link.split('<https')
65
+ last_link = aa.find{|e| e=~ /rel="last"/}
66
+ last_link.to_s =~ /\?page=(\d+)/
67
+ $1
68
+ end
69
+
70
+ end
71
+
72
+ ## Instantiate your wrapped API
73
+
74
+ api = ApiBee.setup(GithubAdapter)
75
+
76
+ repos = api.get('/users/ismasan/repos')
77
+
78
+ def show(data, c)
79
+ puts "+++++++++++++++ Page #{data.current_page} of #{data.total_pages} (#{data.total_entries} entries). #{data.size} now. ++++++++"
80
+ puts
81
+ data.each do |pp|
82
+ puts "#{c}.- #{pp[:id]}: #{pp[:name]}. Forks: #{pp[:forks]}. URL: #{pp[:url]}"
83
+ c += 1
84
+ end
85
+ puts
86
+ p [:next, data.next_page, data.total_entries]
87
+ if data.has_next_page? # recurse
88
+ show data.paginate(:page => data.next_page, :per_page => 20), c
89
+ end
90
+ end
91
+
92
+ ## Auto paginate over all the repos
93
+
94
+ show repos, 1
95
+
96
+
97
+ puts "First created at is: #{repos.first[:created_at]}"
98
+
99
+ # Fetch a single node
100
+ one = repos.find_one('https://api.github.com/repos/ismasan/websockets_examples')
101
+
102
+ # one[:owner][:public_repos] will trigger a new request to the resource URL because that property is not available in the excerpt
103
+ #
104
+ puts "An owner is #{one[:owner][:login]}, who has #{one[:owner][:public_repos]} public repos"
@@ -4,9 +4,17 @@ require 'api_bee'
4
4
 
5
5
  api = ApiBee.setup(:hash, YAML.load_file('./catalog.yml'))
6
6
 
7
+ collections = api.get('/collections')
8
+
9
+ #p collections
10
+
11
+ p collections.current_page
12
+ # p collections.size
13
+
7
14
  collection = api.get('/collections/c1')
8
15
 
9
16
  p collection[:title]
17
+ p collection[:id]
10
18
 
11
19
  products = collection[:products]
12
20
 
@@ -25,6 +25,10 @@ module ApiBee
25
25
  found
26
26
  end
27
27
 
28
+ def find_one(href, id)
29
+ get("#{href}/#{id}")
30
+ end
31
+
28
32
  protected
29
33
 
30
34
  def parse_href(href)
@@ -60,7 +64,7 @@ module ApiBee
60
64
  from = page * per_page - per_page
61
65
  to = page * per_page
62
66
  list[:entries] = list[:entries].to_a[from...to]
63
- list[:current_page] = page
67
+ list[:page] = page
64
68
  list[:per_page] = per_page
65
69
  list
66
70
  end
data/lib/api_bee/node.rb CHANGED
@@ -2,25 +2,40 @@ module ApiBee
2
2
 
3
3
  class Node
4
4
 
5
- def self.resolve(adapter, attrs)
5
+ def self.simbolized(hash)
6
+ hash.inject({}) do |options, (key, value)|
7
+ options[(key.to_sym rescue key) || key] = value
8
+ options
9
+ end
10
+ end
11
+
12
+ def self.resolve(adapter, attrs, href = nil)
13
+ attrs = simbolized(attrs)
6
14
  keys = attrs.keys.map{|k| k.to_sym}
7
- if keys.include?(:total_entries) && keys.include?(ApiBee.config.uri_property_name) # is a paginator
8
- List.new adapter, attrs
15
+ if keys.include?(:total_entries) && keys.include?(ApiBee.config.uri_property_name.to_sym) # is a paginator
16
+ List.new adapter, attrs, href
9
17
  else
10
- Single.new adapter, attrs
18
+ Single.new adapter, attrs, href
11
19
  end
12
20
  end
13
21
 
14
22
  attr_reader :adapter
15
23
 
16
- def initialize(adapter, attrs)
24
+ def initialize(adapter, attrs, href)
17
25
  @adapter = adapter
18
26
  @attributes = {}
27
+ @href = href
19
28
  update_attributes attrs
20
29
  end
21
30
 
31
+ def to_data
32
+ @attributes
33
+ end
34
+
22
35
  def [](attribute_name)
23
- if value = @attributes[attribute_name]
36
+ if respond_to?(attribute_name)
37
+ send attribute_name
38
+ elsif value = @attributes[attribute_name]
24
39
  resolve_values_to_nodes value
25
40
  elsif has_more? # check whether there's more info in API
26
41
  load_more!
@@ -53,7 +68,7 @@ module ApiBee
53
68
 
54
69
  def load_more!
55
70
  more_data = @adapter.get(@attributes[ApiBee.config.uri_property_name])
56
- update_attributes more_data if more_data
71
+ update_attributes Node.simbolized(more_data) if more_data
57
72
  @complete = true
58
73
  end
59
74
 
@@ -65,16 +80,21 @@ module ApiBee
65
80
 
66
81
  DEFAULT_PER_PAGE = 100
67
82
 
83
+ def find_one(id)
84
+ data = @adapter.find_one(@href, id)
85
+ data.nil? ? nil : Node.resolve(@adapter, data, @href)
86
+ end
87
+
68
88
  def total_entries
69
89
  @attributes[:total_entries].to_i
70
90
  end
71
91
 
72
92
  def size
73
- entries.size
93
+ __entries.size
74
94
  end
75
95
 
76
96
  def current_page
77
- (@attributes[:current_page] || 1).to_i
97
+ (@attributes[:page] || 1).to_i
78
98
  end
79
99
 
80
100
  def per_page
@@ -82,7 +102,7 @@ module ApiBee
82
102
  end
83
103
 
84
104
  def total_pages
85
- div = total_entries / per_page
105
+ div = (total_entries.to_f / per_page.to_f).ceil
86
106
  div < 1 ? 1 : div
87
107
  end
88
108
 
@@ -107,29 +127,29 @@ module ApiBee
107
127
  end
108
128
 
109
129
  def first
110
- entries.first
130
+ __entries.first
111
131
  end
112
132
 
113
133
  def last
114
- entries.last
134
+ __entries.last
115
135
  end
116
136
 
117
137
  def each(&block)
118
- entries.each(&block)
138
+ __entries.each(&block)
119
139
  end
120
140
 
121
141
  def each_with_index(&block)
122
- entries.each_with_index(&block)
142
+ __entries.each_with_index(&block)
123
143
  end
124
144
 
125
145
  def paginate(options = {})
126
146
  data = @adapter.get(@attributes[ApiBee.config.uri_property_name], options)
127
- Node.resolve @adapter, data
147
+ Node.resolve @adapter, data, @href
128
148
  end
129
149
 
130
150
  protected
131
151
 
132
- def entries
152
+ def __entries
133
153
  @entries ||= (self[:entries] || [])
134
154
  end
135
155
 
data/lib/api_bee/proxy.rb CHANGED
@@ -4,12 +4,41 @@ module ApiBee
4
4
 
5
5
  attr_reader :adapter
6
6
 
7
- def initialize(adapter)
7
+ def initialize(adapter, href = nil, opts = nil)
8
8
  @adapter = adapter
9
+ @href = href
10
+ @opts = opts
9
11
  end
10
12
 
11
- def get(href)
12
- Node.resolve @adapter, ApiBee.config.uri_property_name => href
13
+ def get(href, opts = {})
14
+ # Just delegate. No API calls at this point. We only load data when we need it.
15
+ Proxy.new @adapter, href, opts
16
+ end
17
+
18
+ def [](key)
19
+ _node[key]
20
+ end
21
+
22
+ def to_data
23
+ _node.to_data
24
+ end
25
+
26
+ def paginate(*args)
27
+ @list ||= Node::List.new(@adapter, {ApiBee.config.uri_property_name => @href}, @href)
28
+ @list.paginate *args
29
+ end
30
+
31
+ protected
32
+
33
+ def method_missing(method_name, *args, &block)
34
+ _node.send(method_name, *args, &block)
35
+ end
36
+
37
+ def _node
38
+ @node ||= (
39
+ data = @adapter.get(@href, @opts)
40
+ Node.resolve @adapter, data, @href
41
+ )
13
42
  end
14
43
 
15
44
  end
@@ -1,3 +1,3 @@
1
1
  module ApiBee
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -12,8 +12,8 @@ describe ApiBee::Adapters::Hash do
12
12
  :id => 'catalog',
13
13
  :foos => [1,2,3,4],
14
14
  :products => {
15
- :total_entries => 4,
16
- :current_page => 1,
15
+ :total_entries => 5,
16
+ :page => 1,
17
17
  :per_page => 4,
18
18
  :href => '/collections/catalog/products',
19
19
  :entries => [
@@ -35,7 +35,12 @@ describe ApiBee::Adapters::Hash do
35
35
  :id => 'foo-4',
36
36
  :title => 'Foo 4',
37
37
  :href => '/products/foo-4'
38
- }
38
+ },
39
+ {
40
+ :id => 'foo-5',
41
+ :title => 'Foo 5',
42
+ :href => '/products/foo-5'
43
+ }
39
44
  ]
40
45
  }
41
46
  }
@@ -45,6 +50,12 @@ describe ApiBee::Adapters::Hash do
45
50
  @adapter = ApiBee::Adapters::Hash.new(@data)
46
51
  end
47
52
 
53
+ context 'non-existing data' do
54
+ it 'should return nil' do
55
+ @adapter.get('/foo/lolz').should be_nil
56
+ end
57
+ end
58
+
48
59
  context 'accessing single nodes' do
49
60
 
50
61
  before do
@@ -64,20 +75,31 @@ describe ApiBee::Adapters::Hash do
64
75
  it 'should return whole collection if no pagination' do
65
76
  products = @adapter.get('/collections/catalog/products')
66
77
  # products.should == 1
67
- products[:current_page].should == 1
68
- products[:total_entries].should == 4
78
+ products[:page].should == 1
79
+ products[:total_entries].should == 5
69
80
  products[:per_page].should == 4
70
- products[:entries].size.should == 4
81
+ products[:entries].size.should == 5
71
82
  end
72
83
 
73
- it 'should paginate paginated collections' do
74
- products = @adapter.get('/collections/catalog/products', :page => 2, :per_page => 2)
75
- products[:current_page].should == 2
76
- products[:total_entries].should == 4
77
- products[:per_page].should == 2
78
- products[:entries].size.should == 2
79
- products[:entries][0][:title].should == 'Foo 3'
80
- products[:entries][1][:title].should == 'Foo 4'
84
+ describe 'paginating' do
85
+ it 'should paginate paginated collections' do
86
+ products = @adapter.get('/collections/catalog/products', :page => 2, :per_page => 2)
87
+ products[:page].should == 2
88
+ products[:total_entries].should == 5
89
+ products[:per_page].should == 2
90
+ products[:entries].size.should == 2
91
+ products[:entries][0][:title].should == 'Foo 3'
92
+ products[:entries][1][:title].should == 'Foo 4'
93
+ end
94
+
95
+ it 'should paginate last page correctly' do
96
+ products = @adapter.get('/collections/catalog/products', :page => 3, :per_page => 2)
97
+ products[:page].should == 3
98
+ products[:total_entries].should == 5
99
+ products[:per_page].should == 2
100
+ products[:entries].size.should == 1
101
+ products[:entries][0][:title].should == 'Foo 5'
102
+ end
81
103
  end
82
104
  end
83
105
 
data/spec/node_spec.rb CHANGED
@@ -7,7 +7,7 @@ describe ApiBee do
7
7
  # Products
8
8
  :products => {
9
9
  :href => '/products',
10
- :total_entries => 6,
10
+ :total_entries => 7,
11
11
  :entries => [
12
12
  {
13
13
  :title => 'Foo 1',
@@ -44,6 +44,12 @@ describe ApiBee do
44
44
  :id => 'foo-6',
45
45
  :price => 600,
46
46
  :description => 'Foo 6 desc'
47
+ },
48
+ {
49
+ :title => 'Foo 7',
50
+ :id => 'foo-7',
51
+ :price => 700,
52
+ :description => 'Foo 7 desc'
47
53
  }
48
54
  ]
49
55
  },
@@ -55,7 +61,7 @@ describe ApiBee do
55
61
  :products => {
56
62
  :href => '/products',
57
63
  :total_entries => 4,
58
- :current_page => 1,
64
+ :page => 1,
59
65
  :per_page => 2,
60
66
  :href => '/products',
61
67
  :entries => [
@@ -89,6 +95,19 @@ describe ApiBee do
89
95
  node.should be_kind_of(ApiBee::Node::Single)
90
96
  end
91
97
 
98
+ it 'should symbolize hash keys' do
99
+ node = ApiBee::Node.resolve(@adapter, {
100
+ 'title' => 'Blah',
101
+ 'total_entries' => 4,
102
+ 'href' => '/products',
103
+ 'foos' => [1,2,3,4]
104
+ })
105
+ node.total_entries.should == 4
106
+ node[:foos].should == [1,2,3,4]
107
+ node.adapter.should == @adapter
108
+ node.should be_kind_of(ApiBee::Node::List)
109
+ end
110
+
92
111
  it 'should resolve paginated lists' do
93
112
  node = ApiBee::Node.resolve(@adapter, {:title => 'Blah', :total_entries => 4, :href => '/products'})
94
113
  node.total_entries.should == 4
@@ -112,14 +131,83 @@ describe ApiBee do
112
131
  describe 'single nodes' do
113
132
  it 'should call adapter only when accessing needed attributes' do
114
133
  hash = @data[:collections].last
115
- @adapter.should_receive(:get).exactly(1).times.with('/collections/catalog').and_return hash
134
+ @adapter.should_receive(:get).exactly(1).times.with('/collections/catalog', {}).and_return hash
116
135
  node = @api.get('/collections/catalog')
117
136
  node[:title].should == 'Catalog'
118
137
  node[:id].should == 'catalog'
119
138
  end
120
139
  end
121
140
 
122
- describe 'paginated lists' do
141
+ describe '#find_one' do
142
+
143
+ before do
144
+ @products = @api.get('/products', :page => 1, :per_page => 2)
145
+ end
146
+
147
+ it 'should delegate to adapter. It knows how to find individual resoruces' do
148
+ @adapter.should_receive(:find_one).with('/products', 'foo-1')
149
+ @products.find_one('foo-1')
150
+ end
151
+
152
+ it 'should return a Node::Single' do
153
+ node = @products.find_one('foo-1')
154
+ node.should be_kind_of(ApiBee::Node::Single)
155
+ node[:title].should == 'Foo 1'
156
+ end
157
+
158
+ it 'should return nil if not found' do
159
+ @products.find_one('foo-1000').should be_nil
160
+ end
161
+
162
+ end
163
+
164
+ end
165
+
166
+ context 'navigating data' do
167
+
168
+ before do
169
+ @api = ApiBee.setup(:hash, @data)
170
+ end
171
+
172
+ describe '#to_data' do
173
+ it 'should return raw data' do
174
+ d = @api.get('/collections/catalog').to_data
175
+ d.should be_kind_of(::Hash)
176
+ d.should == @data[:collections].first
177
+ end
178
+ end
179
+
180
+ describe 'paginated root level lists' do
181
+
182
+ before do
183
+ @products = @api.get('/products', :page => 1, :per_page => 2)
184
+ end
185
+
186
+ it 'should have a paginator interface' do
187
+
188
+ @products.total_entries.should == 7
189
+ @products.size.should == 2
190
+ @products.total_pages.should == 4
191
+ @products.current_page.should == 1
192
+ @products.pages.should == [1,2,3,4]
193
+ @products.has_next_page?.should be_true
194
+ @products.has_prev_page?.should be_false
195
+ end
196
+
197
+ it 'should paginate last page correctly' do
198
+ last_page = @products.paginate(:per_page => 2, :page =>4)
199
+
200
+ last_page.total_entries.should == 7
201
+ last_page.size.should == 1
202
+ last_page.total_pages.should == 4
203
+ last_page.current_page.should == 4
204
+ last_page.pages.should == [1,2,3,4]
205
+ last_page.has_next_page?.should be_false
206
+ last_page.has_prev_page?.should be_true
207
+ end
208
+ end
209
+
210
+ describe 'paginated nested lists' do
123
211
  before do
124
212
  @collection = @api.get('/collections/catalog')
125
213
  @products = @collection[:products]
@@ -153,7 +241,7 @@ describe ApiBee do
153
241
  @products.each {|p| titles << p[:title]}
154
242
  @products.each {|p| klasses << p.class}
155
243
  @products.current_page.should == 2
156
- @products.total_entries.should == 6
244
+ @products.total_entries.should == 7
157
245
  @products.size.should == 2
158
246
  klasses.should == [ApiBee::Node::Single, ApiBee::Node::Single]
159
247
  titles.should == ['Foo 3', 'Foo 4']
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.1
5
+ version: 0.0.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Ismael Celis
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-09-19 00:00:00 Z
13
+ date: 2011-09-21 00:00:00 Z
14
14
  dependencies: []
15
15
 
16
16
  description: API Bee is a small client / spec for a particular style of JSON API. USe Hash adapter for local data access.
@@ -30,6 +30,7 @@ files:
30
30
  - Rakefile
31
31
  - api_bee.gemspec
32
32
  - examples/catalog.yml
33
+ - examples/github_api.rb
33
34
  - examples/yaml_catalog.rb
34
35
  - lib/api_bee.rb
35
36
  - lib/api_bee/adapters/hash.rb