api_bee 0.0.1 → 0.0.2

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
@@ -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