api_bee 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.mkd CHANGED
@@ -61,19 +61,33 @@ api.get('/my/resources').each do |r|
61
61
  end
62
62
  ```
63
63
 
64
+ ## Delegate to adapter
65
+
66
+ Lazy-loading and paginating resources is great for GET requests, but you might want to still use your adapter's other methods.
67
+
68
+ api = ApiBee.setup(MyCustomAdapter) do |config|
69
+ config.expose :delete, :post
70
+ end
71
+
72
+ # This still wraps your adapter's get() method and adds lazy-loading and pagination
73
+ api.get('/products').first[:title]
74
+
75
+ # This delegates directory to MyCustomAdapter#post()
76
+ api.post('/products', :title => 'Foo', :price => 100.0)
77
+
64
78
  ## finding a single resource
65
79
 
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.
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.
67
81
 
68
82
  resources = api.get('/my/resources')
69
- resource = resources.find_one('foobar')
83
+ resource = resources.get_one('foobar')
70
84
 
71
- That delegates to Adapter#find_one passing 2 arguments: the list's href and the passed name or identifier, so:
85
+ That delegates to Adapter#get_one passing 2 arguments: the list's href and the passed name or identifier, so:
72
86
 
73
87
  ```ruby
74
88
  class ApiBee::Adapters::Special
75
89
  # ...
76
- def find_one(href, id)
90
+ def get_one(href, id)
77
91
  get "#{href}/#{id}"
78
92
  end
79
93
  end
@@ -1,14 +1,15 @@
1
1
  $LOAD_PATH.unshift '../lib'
2
2
  require 'api_bee'
3
- #require 'net/http'
4
3
  require 'net/https'
5
4
  require 'uri'
6
5
  require 'json'
7
6
 
7
+ # Github adapter wraps raw Github API response and decorates collections with
8
+ # pagination parameters needed by APIBee
9
+ #
8
10
  class GithubAdapter
9
11
 
10
12
  def initialize
11
- ApiBee.config.uri_property_name = :url
12
13
  @url = URI.parse('https://api.github.com')
13
14
  @http = Net::HTTP.new(@url.host, @url.port)
14
15
  @http.use_ssl = true
@@ -16,8 +17,6 @@ class GithubAdapter
16
17
  end
17
18
 
18
19
  def get(path, options = {})
19
- per_page = (options[:per_page] || 20).to_i
20
- page = (options[:page] || 1).to_i
21
20
 
22
21
  q = options.map{|k,v| "#{k}=#{v}"}.join('&')
23
22
 
@@ -28,21 +27,9 @@ class GithubAdapter
28
27
 
29
28
  if response.kind_of?(Net::HTTPOK)
30
29
  results = JSON.parse response.body
31
- if results.is_a?(Array)
30
+ results = if results.is_a?(Array)
32
31
  # 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
32
+ paginate results, options, path, response
46
33
  else
47
34
  results
48
35
  end
@@ -54,12 +41,32 @@ class GithubAdapter
54
41
 
55
42
  end
56
43
 
57
- def find_one(href, id)
44
+ def get_one(href, id)
58
45
  get id
59
46
  end
60
47
 
61
48
  protected
62
49
 
50
+ def paginate(results, options, path, response)
51
+ per_page = (options[:per_page] || 20).to_i
52
+ page = (options[:page] || 1).to_i
53
+
54
+ results = {
55
+ :entries => results,
56
+ :page => page,
57
+ :per_page => per_page,
58
+ :url => path
59
+ }
60
+ # Extract last page number and entries count. Github uses a 'Link' header
61
+ if link = response["link"]
62
+ last_page = extract_last_page(link) || page # if no 'last' link, we're on the last page
63
+ results.update(
64
+ :total_entries => last_page.to_i * per_page
65
+ )
66
+ end
67
+ results
68
+ end
69
+
63
70
  def extract_last_page(link)
64
71
  aa = link.split('<https')
65
72
  last_link = aa.find{|e| e=~ /rel="last"/}
@@ -69,12 +76,17 @@ class GithubAdapter
69
76
 
70
77
  end
71
78
 
79
+ ########### USAGE ##################################
80
+
72
81
  ## Instantiate your wrapped API
73
82
 
74
- api = ApiBee.setup(GithubAdapter)
83
+ api = ApiBee.setup(GithubAdapter) do |config|
84
+ config.uri_property_name = :url
85
+ end
75
86
 
76
87
  repos = api.get('/users/ismasan/repos')
77
88
 
89
+ # Recursive method prints results for each page
78
90
  def show(data, c)
79
91
  puts "+++++++++++++++ Page #{data.current_page} of #{data.total_pages} (#{data.total_entries} entries). #{data.size} now. ++++++++"
80
92
  puts
@@ -97,7 +109,7 @@ show repos, 1
97
109
  puts "First created at is: #{repos.first[:created_at]}"
98
110
 
99
111
  # Fetch a single node
100
- one = repos.find_one('https://api.github.com/repos/ismasan/websockets_examples')
112
+ one = repos.get_one('https://api.github.com/repos/ismasan/websockets_examples')
101
113
 
102
114
  # one[:owner][:public_repos] will trigger a new request to the resource URL because that property is not available in the excerpt
103
115
  #
@@ -25,7 +25,7 @@ module ApiBee
25
25
  found
26
26
  end
27
27
 
28
- def find_one(href, id)
28
+ def get_one(href, id)
29
29
  get("#{href}/#{id}")
30
30
  end
31
31
 
@@ -57,7 +57,7 @@ module ApiBee
57
57
  end
58
58
 
59
59
  def is_paginated?(hash)
60
- hash[ApiBee.config.uri_property_name] && hash[:total_entries]
60
+ hash[:href] && hash[:total_entries]
61
61
  end
62
62
 
63
63
  def paginate(list, page, per_page)
data/lib/api_bee/node.rb CHANGED
@@ -9,20 +9,21 @@ module ApiBee
9
9
  end
10
10
  end
11
11
 
12
- def self.resolve(adapter, attrs, href = nil)
12
+ def self.resolve(adapter, config, attrs, href = nil)
13
13
  attrs = simbolized(attrs)
14
14
  keys = attrs.keys.map{|k| k.to_sym}
15
- if keys.include?(:total_entries) && keys.include?(ApiBee.config.uri_property_name.to_sym) # is a paginator
16
- List.new adapter, attrs, href
15
+ if keys.include?(config.total_entries_property_name) && keys.include?(config.uri_property_name.to_sym) # is a paginator
16
+ List.new adapter, config, attrs, href
17
17
  else
18
- Single.new adapter, attrs, href
18
+ Single.new adapter, config, attrs, href
19
19
  end
20
20
  end
21
21
 
22
22
  attr_reader :adapter
23
23
 
24
- def initialize(adapter, attrs, href)
24
+ def initialize(adapter, config, attrs, href)
25
25
  @adapter = adapter
26
+ @config = config
26
27
  @attributes = {}
27
28
  @href = href
28
29
  update_attributes attrs
@@ -54,7 +55,7 @@ module ApiBee
54
55
  def resolve_values_to_nodes(value)
55
56
  case value
56
57
  when ::Hash
57
- Node.resolve @adapter, value
58
+ Node.resolve @adapter, @config, value
58
59
  when ::Array
59
60
  value.map {|v| resolve_values_to_nodes(v)} # recurse
60
61
  else
@@ -63,11 +64,11 @@ module ApiBee
63
64
  end
64
65
 
65
66
  def has_more?
66
- !@complete && @attributes[ApiBee.config.uri_property_name]
67
+ !@complete && @attributes[@config.uri_property_name]
67
68
  end
68
69
 
69
70
  def load_more!
70
- more_data = @adapter.get(@attributes[ApiBee.config.uri_property_name])
71
+ more_data = @adapter.get(@attributes[@config.uri_property_name])
71
72
  update_attributes Node.simbolized(more_data) if more_data
72
73
  @complete = true
73
74
  end
@@ -80,9 +81,9 @@ module ApiBee
80
81
 
81
82
  DEFAULT_PER_PAGE = 100
82
83
 
83
- def find_one(id)
84
- data = @adapter.find_one(@href, id)
85
- data.nil? ? nil : Node.resolve(@adapter, data, @href)
84
+ def get_one(id)
85
+ data = @adapter.get_one(@href, id)
86
+ data.nil? ? nil : Node.resolve(@adapter, @config, data, @href)
86
87
  end
87
88
 
88
89
  def total_entries
@@ -143,14 +144,14 @@ module ApiBee
143
144
  end
144
145
 
145
146
  def paginate(options = {})
146
- data = @adapter.get(@attributes[ApiBee.config.uri_property_name], options)
147
- Node.resolve @adapter, data, @href
147
+ data = @adapter.get(@attributes[@config.uri_property_name], options)
148
+ Node.resolve @adapter, @config, data, @href
148
149
  end
149
150
 
150
151
  protected
151
152
 
152
153
  def __entries
153
- @entries ||= (self[:entries] || [])
154
+ @entries ||= (self[@config.entries_property_name] || [])
154
155
  end
155
156
 
156
157
  end
data/lib/api_bee/proxy.rb CHANGED
@@ -4,15 +4,16 @@ module ApiBee
4
4
 
5
5
  attr_reader :adapter
6
6
 
7
- def initialize(adapter, href = nil, opts = nil)
7
+ def initialize(adapter, config, href = nil, opts = nil)
8
8
  @adapter = adapter
9
+ @config = config
9
10
  @href = href
10
11
  @opts = opts
11
12
  end
12
13
 
13
14
  def get(href, opts = {})
14
15
  # Just delegate. No API calls at this point. We only load data when we need it.
15
- Proxy.new @adapter, href, opts
16
+ Proxy.new @adapter, @config, href, opts
16
17
  end
17
18
 
18
19
  def [](key)
@@ -24,20 +25,32 @@ module ApiBee
24
25
  end
25
26
 
26
27
  def paginate(*args)
27
- @list ||= Node::List.new(@adapter, {ApiBee.config.uri_property_name => @href}, @href)
28
+ @list ||= Node::List.new(@adapter, @config, {@config.uri_property_name => @href}, @href)
28
29
  @list.paginate *args
29
30
  end
30
31
 
32
+ def ==(other)
33
+ _node.to_data
34
+ end
35
+
31
36
  protected
32
37
 
33
38
  def method_missing(method_name, *args, &block)
34
- _node.send(method_name, *args, &block)
39
+ if _adapter_delegators.include?(method_name)
40
+ @adapter.send(method_name, *args, &block)
41
+ else
42
+ _node.send(method_name, *args, &block)
43
+ end
44
+ end
45
+
46
+ def _adapter_delegators
47
+ @adapter_delegators ||= @config.adapter_delegators || []
35
48
  end
36
49
 
37
50
  def _node
38
51
  @node ||= (
39
52
  data = @adapter.get(@href, @opts)
40
- Node.resolve @adapter, data, @href
53
+ Node.resolve @adapter, @config, data, @href
41
54
  )
42
55
  end
43
56
 
@@ -1,3 +1,3 @@
1
1
  module ApiBee
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
data/lib/api_bee.rb CHANGED
@@ -6,8 +6,6 @@ module ApiBee
6
6
 
7
7
  def setup(adapter_klass, *args)
8
8
 
9
- yield config if block_given?
10
-
11
9
  adapter = if adapter_klass.is_a?(::Symbol)
12
10
  require File.join('api_bee', 'adapters', adapter_klass.to_s)
13
11
  klass = adapter_klass.to_s.gsub(/(^.{1})/){$1.upcase}
@@ -17,18 +15,37 @@ module ApiBee
17
15
  end
18
16
 
19
17
  raise NoMethodError, "Adapter must implement #get(path, *args) method" unless adapter.respond_to?(:get)
20
- Proxy.new adapter
18
+
19
+ config = new_config
20
+ yield config if block_given?
21
+ Proxy.new adapter, config
22
+ end
23
+
24
+ # new config object with defaults
25
+ def new_config
26
+ Config.new(
27
+ # This field is expected in API responses
28
+ # and should point to an individual resource with more data
29
+ :uri_property_name => :href,
30
+ # Total number of entries
31
+ # Used to paginate lists
32
+ :total_entries_property_name => :total_entries,
33
+ # Name of array property
34
+ # that contains cureent page's entries
35
+ :entries_property_name => :entries
36
+ )
21
37
  end
22
38
 
23
- def config
24
- @config ||= OpenStruct.new
39
+ class Config < OpenStruct
40
+
41
+ def expose(*fields)
42
+ self.adapter_delegators = fields
43
+ end
44
+
25
45
  end
26
46
 
27
47
  end
28
48
 
29
- # Defaults
30
- self.config.uri_property_name = :href
31
-
32
49
  module Adapters
33
50
 
34
51
  end
data/spec/node_spec.rb CHANGED
@@ -86,17 +86,18 @@ describe ApiBee do
86
86
 
87
87
  before do
88
88
  @adapter = mock('Adapter')
89
+ @config = ApiBee.new_config
89
90
  end
90
91
 
91
92
  it 'should resolve single nodes' do
92
- node = ApiBee::Node.resolve(@adapter, {:title => 'Blah', :foo => [1,2,3]})
93
+ node = ApiBee::Node.resolve(@adapter, @config, {:title => 'Blah', :foo => [1,2,3]})
93
94
  node[:title].should == 'Blah'
94
95
  node.adapter.should == @adapter
95
96
  node.should be_kind_of(ApiBee::Node::Single)
96
97
  end
97
98
 
98
99
  it 'should symbolize hash keys' do
99
- node = ApiBee::Node.resolve(@adapter, {
100
+ node = ApiBee::Node.resolve(@adapter, @config, {
100
101
  'title' => 'Blah',
101
102
  'total_entries' => 4,
102
103
  'href' => '/products',
@@ -109,7 +110,7 @@ describe ApiBee do
109
110
  end
110
111
 
111
112
  it 'should resolve paginated lists' do
112
- node = ApiBee::Node.resolve(@adapter, {:title => 'Blah', :total_entries => 4, :href => '/products'})
113
+ node = ApiBee::Node.resolve(@adapter, @config, {:title => 'Blah', :total_entries => 4, :href => '/products'})
113
114
  node.total_entries.should == 4
114
115
  node.adapter.should == @adapter
115
116
  node.should be_kind_of(ApiBee::Node::List)
@@ -138,25 +139,25 @@ describe ApiBee do
138
139
  end
139
140
  end
140
141
 
141
- describe '#find_one' do
142
+ describe '#get_one' do
142
143
 
143
144
  before do
144
145
  @products = @api.get('/products', :page => 1, :per_page => 2)
145
146
  end
146
147
 
147
148
  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')
149
+ @adapter.should_receive(:get_one).with('/products', 'foo-1')
150
+ @products.get_one('foo-1')
150
151
  end
151
152
 
152
153
  it 'should return a Node::Single' do
153
- node = @products.find_one('foo-1')
154
+ node = @products.get_one('foo-1')
154
155
  node.should be_kind_of(ApiBee::Node::Single)
155
156
  node[:title].should == 'Foo 1'
156
157
  end
157
158
 
158
159
  it 'should return nil if not found' do
159
- @products.find_one('foo-1000').should be_nil
160
+ @products.get_one('foo-1000').should be_nil
160
161
  end
161
162
 
162
163
  end
data/spec/setup_spec.rb CHANGED
@@ -10,23 +10,6 @@ describe 'ApiBee.setup' do
10
10
  end
11
11
  end
12
12
 
13
- describe 'config block' do
14
-
15
- it 'should have default uri_property_name field name' do
16
- ApiBee.config.uri_property_name.should == :href
17
- end
18
-
19
- it 'should set global variables' do
20
- api = ApiBee.setup(:hash, {}) do |config|
21
- config.foo = 11
22
- config.bar = 22
23
- end
24
-
25
- ApiBee.config.foo.should == 11
26
- ApiBee.config.bar.should == 22
27
- end
28
- end
29
-
30
13
  describe 'with custom adapter class' do
31
14
  before do
32
15
 
@@ -57,4 +40,96 @@ describe 'ApiBee.setup' do
57
40
  end
58
41
 
59
42
  end
43
+
44
+ context 'configuration' do
45
+
46
+ before do
47
+ @api1 = ApiBee.setup(:hash, {
48
+ :user => {
49
+ :name => 'ismael 1',
50
+ :href => '/users/ismael1'
51
+ }
52
+ })
53
+
54
+ @api2 = ApiBee.setup(:hash, {
55
+ :user => {
56
+ :name => 'ismael 2',
57
+ :url => '/users/ismael2'
58
+ }
59
+ }) do |config|
60
+ config.uri_property_name = :url
61
+ end
62
+ end
63
+
64
+ describe 'API config' do
65
+
66
+ it 'should produce a config object with default values' do
67
+ config = ApiBee.new_config
68
+ config.uri_property_name.should == :href
69
+ config.total_entries_property_name.should == :total_entries
70
+ config.entries_property_name.should == :entries
71
+ end
72
+
73
+ it 'should have default :href configured' do
74
+ user = @api1.get('/user')
75
+ user[:name].should == 'ismael 1'
76
+ @api1.adapter.should_receive(:get).with('/users/ismael1').and_return(:last_name => 'Celis')
77
+
78
+ user[:last_name].should == 'Celis'
79
+ end
80
+
81
+ it 'should overwrite config for individual apis' do
82
+ user1 = @api1.get('/user')
83
+ user1[:name].should == 'ismael 1'
84
+ @api1.adapter.should_receive(:get).with('/users/ismael1').and_return(:last_name => 'Celis 1')
85
+
86
+ user1[:last_name].should == 'Celis 1'
87
+
88
+ user2 = @api2.get('/user')
89
+ user2[:name].should == 'ismael 2'
90
+ @api2.adapter.should_receive(:get).with('/users/ismael2').and_return(:last_name => 'Celis 2')
91
+
92
+ user2[:last_name].should == 'Celis 2'
93
+ end
94
+
95
+ end
96
+
97
+ describe 'delegate to adapter' do
98
+
99
+ before do
100
+
101
+ adapter = Class.new(ApiBee::Adapters::Hash) do
102
+ def fetch(*args)
103
+ @data.fetch *args
104
+ end
105
+
106
+ def keys(*args)
107
+ @data.keys *args
108
+ end
109
+ end
110
+
111
+ @api = ApiBee.setup(adapter, {
112
+ :a => {:name => 1},
113
+ :b => {:name => 2}
114
+ }) do |config|
115
+ config.expose :fetch, :keys
116
+ end
117
+
118
+ end
119
+
120
+ it 'should still work' do
121
+ @api.get('/a').should == {:name => 1}
122
+ @api.get('/b').should == {:name => 2}
123
+ end
124
+
125
+ it 'should delegate configured methods on to adapter' do
126
+ @api.fetch(:a).should == {:name => 1}
127
+ @api.fetch(:x, 'X').should == 'X'
128
+
129
+ @api.keys.should == [:a, :b]
130
+ end
131
+
132
+ end
133
+ end
134
+
60
135
  end
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.2
5
+ version: 0.0.3
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-21 00:00:00 Z
13
+ date: 2011-09-22 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.