api_bee 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -5,5 +5,6 @@ gemspec
5
5
 
6
6
  group :test do
7
7
  gem 'rspec' # dev, test
8
+ gem 'rake'
8
9
  end
9
10
 
data/README.mkd CHANGED
@@ -179,6 +179,43 @@ api.get('/products').first[:title]
179
179
  api.post('/products', :title => 'Foo', :price => 100.0)
180
180
  ```
181
181
 
182
+ ## Wrapping nodes
183
+ You can wrap nodes in collections in your custom classes. If your adapter defines a `#wrap` method, it will be passed each single node in your data.
184
+
185
+ ```ruby
186
+ class ProductWrapper
187
+ def initialize(node)
188
+ @node = node
189
+ end
190
+
191
+ def name
192
+ @node[:name]
193
+ end
194
+ end
195
+
196
+ class MyCustomAdapter
197
+
198
+ def get(*args)
199
+ # ...
200
+ end
201
+
202
+ # Wrap product objects
203
+ def wrap(node, name)
204
+ if name =~ /product/
205
+ ProductWrapper.new node
206
+ else
207
+ node
208
+ end
209
+ end
210
+
211
+ end
212
+
213
+ api = ApiBee.setup(MyCustomAdapter)
214
+
215
+ object = api.get('/products').first #=> an instance of ProductWrapper
216
+
217
+ object.name # => delegates to ApiBee::Node::Single#[]
218
+ ````
182
219
  ## finding a single resource
183
220
 
184
221
  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.
@@ -6,26 +6,38 @@
6
6
  :title: P1
7
7
  :id: p1
8
8
  :price: 100
9
+ :category:
10
+ :name: music players
9
11
  -
10
12
  :title: P2
11
13
  :id: p2
12
14
  :price: 200
15
+ :category:
16
+ :name: ijdeiwejdneidjn
13
17
  -
14
18
  :title: P3
15
19
  :id: p3
16
20
  :price: 300
21
+ :category:
22
+ :name: music players
17
23
  -
18
24
  :title: P4
19
25
  :id: p4
20
26
  :price: 400
27
+ :category:
28
+ :name: music players
21
29
  -
22
30
  :title: P5
23
31
  :id: p5
24
32
  :price: 500
33
+ :category:
34
+ :name: music players
25
35
  -
26
36
  :title: P6
27
37
  :id: p6
28
38
  :price: 600
39
+ :category:
40
+ :name: music players
29
41
 
30
42
  :collections:
31
43
  :total_entries: 2
@@ -9,10 +9,32 @@ require 'json'
9
9
  #
10
10
  class GithubAdapter
11
11
 
12
+ class Owner
13
+ def initialize(node)
14
+ @node = node
15
+ end
16
+
17
+ def login
18
+ @node[:login]
19
+ end
20
+
21
+ def public_repos
22
+ @node[:public_repos]
23
+ end
24
+ end
25
+
12
26
  def self.config_api_bee(config)
13
27
  config.uri_property_name = :url
14
28
  end
15
29
 
30
+ def wrap(node, name)
31
+ if name =~ /owner/
32
+ Owner.new(node)
33
+ else
34
+ node
35
+ end
36
+ end
37
+
16
38
  def initialize
17
39
  @url = URI.parse('https://api.github.com')
18
40
  @http = Net::HTTP.new(@url.host, @url.port)
@@ -33,6 +55,7 @@ class GithubAdapter
33
55
  results = JSON.parse response.body
34
56
  if results.is_a?(Array)
35
57
  # decorate returned array so it complies with APiBee's pagination params
58
+ # p results
36
59
  paginate results, options, path, response
37
60
  else
38
61
  results
@@ -113,4 +136,4 @@ one = repos.get_one('https://api.github.com/repos/ismasan/websockets_examples')
113
136
 
114
137
  # one[:owner][:public_repos] will trigger a new request to the resource URL because that property is not available in the excerpt
115
138
  #
116
- puts "An owner is #{one[:owner][:login]}, who has #{one[:owner][:public_repos]} public repos"
139
+ puts "An owner is #{one[:owner].login}, who has #{one[:owner].public_repos} public repos"
@@ -2,9 +2,59 @@ $LOAD_PATH.unshift '../lib'
2
2
  require 'yaml'
3
3
  require 'api_bee'
4
4
 
5
- api = ApiBee.setup(:hash, YAML.load_file('./catalog.yml'))
5
+ require 'api_bee/adapters/hash'
6
+
7
+ class Product
8
+ def initialize(node)
9
+ @node = node
10
+ end
11
+
12
+ def title
13
+ @node[:title]
14
+ end
15
+
16
+ def [](key)
17
+ @node[key.to_sym]
18
+ end
19
+ end
20
+
21
+ class Collection < Product
22
+ def collection_title
23
+ [title, @node[:id]].join('_')
24
+ end
25
+ end
26
+
27
+ class Category < Product
28
+ def name
29
+ @node[:name]
30
+ end
31
+ end
32
+
33
+ class Adapter < ApiBee::Adapters::Hash
34
+
35
+ def wrap(node, name)
36
+ puts "NAME is #{name}"
37
+ case name
38
+ when /products/
39
+ Product.new node
40
+ when /collections/
41
+ Collection.new node
42
+ when /categor/
43
+ Category.new node
44
+ else
45
+ node
46
+ end
47
+ end
48
+ end
49
+
50
+ api = ApiBee.setup(Adapter, YAML.load_file(File.dirname(__FILE__)+'/catalog.yml'))
6
51
 
7
52
  collections = api.get('/collections')
53
+ p collections.first.class.name
54
+
55
+ collections.each do |c|
56
+ puts c.collection_title
57
+ end
8
58
 
9
59
  #p collections
10
60
 
@@ -18,10 +68,13 @@ p collection[:id]
18
68
 
19
69
  products = collection[:products]
20
70
 
71
+ p products.first.class.name
72
+
21
73
  puts "First page"
22
74
 
23
75
  products.each do |p|
24
- p p[:title]
76
+ p p.title
77
+ p p[:category].name
25
78
  end
26
79
 
27
80
  puts "Second page"
@@ -15,7 +15,7 @@ module ApiBee
15
15
 
16
16
  def get(href, opts = {})
17
17
  segments = parse_href(href)
18
- segments.inject(data) do |mem,i|
18
+ segments.inject(deep_clone_of(data)) do |mem,i|
19
19
  case mem
20
20
  when ::Hash
21
21
  handle_hash_data mem, i, opts
@@ -33,6 +33,13 @@ module ApiBee
33
33
 
34
34
  protected
35
35
 
36
+ # We don't want to modify the in-memory version of base data, and Ruby's Object.dup makes shallow copies only
37
+ # This is not the most efficient method, but the Hash adapter is for tests only anyway.
38
+ #
39
+ def deep_clone_of(hash)
40
+ Marshal::load(Marshal.dump(hash))
41
+ end
42
+
36
43
  def parse_href(href)
37
44
  href.gsub(/^\//, '').split('/')
38
45
  end
@@ -28,13 +28,15 @@ module ApiBee
28
28
  # node = Node.resolve(an_adapter, config_object, {:name => 'Ismael', :bday => '11/29/77'})
29
29
  # # node is a Node::Single because it doesn't represent a paginated list
30
30
  #
31
- def self.resolve(adapter, config, attrs, href = nil)
31
+ def self.resolve(adapter, config, attrs, attribute_name = nil)
32
32
  attrs = simbolized(attrs)
33
33
  keys = attrs.keys.map{|k| k.to_sym}
34
34
  if keys.include?(config.total_entries_property_name) && keys.include?(config.uri_property_name.to_sym) # is a paginator
35
- List.new adapter, config, attrs, href
35
+ List.new adapter, config, attrs, attribute_name
36
36
  else
37
- Single.new adapter, config, attrs, href
37
+ node = Single.new adapter, config, attrs, attribute_name
38
+ node = adapter.wrap(node, attribute_name) if adapter.respond_to?(:wrap)
39
+ node
38
40
  end
39
41
  end
40
42
 
@@ -72,7 +74,7 @@ module ApiBee
72
74
  #
73
75
  def [](attribute_name)
74
76
  if value = @attributes[attribute_name]
75
- resolve_values_to_nodes value
77
+ resolve_values_to_nodes value, attribute_name
76
78
  elsif has_more? # check whether there's more info in API
77
79
  load_more!
78
80
  self[attribute_name] # recurse once
@@ -85,18 +87,24 @@ module ApiBee
85
87
  @attributes[key.to_sym] = value
86
88
  end
87
89
 
90
+ def has_key?(key)
91
+ return true if @attributes.has_key? key
92
+ load_more! if has_more?
93
+ @attributes.has_key? key
94
+ end
95
+
88
96
  protected
89
97
 
90
98
  def update_attributes(attrs)
91
99
  @attributes.merge!(attrs)
92
100
  end
93
101
 
94
- def resolve_values_to_nodes(value)
102
+ def resolve_values_to_nodes(value, attribute_name)
95
103
  case value
96
104
  when ::Hash
97
- Node.resolve @adapter, @config, value
105
+ Node.resolve @adapter, @config, value, attribute_name
98
106
  when ::Array
99
- value.map {|v| resolve_values_to_nodes(v)} # recurse
107
+ value.map {|v| resolve_values_to_nodes(v, attribute_name)} # recurse
100
108
  else
101
109
  value
102
110
  end
@@ -228,7 +236,11 @@ module ApiBee
228
236
  protected
229
237
 
230
238
  def __entries
231
- @entries ||= (self[@config.entries_property_name] || [])
239
+ @entries ||= begin
240
+ (@attributes[@config.entries_property_name] || []).map do |entry|
241
+ resolve_values_to_nodes entry, @href
242
+ end
243
+ end
232
244
  end
233
245
 
234
246
  end
@@ -1,3 +1,3 @@
1
1
  module ApiBee
2
- VERSION = "0.1.7"
2
+ VERSION = "0.1.8"
3
3
  end
@@ -95,6 +95,7 @@ describe ApiBee::Adapters::Hash do
95
95
  end
96
96
 
97
97
  describe 'paginating' do
98
+ # Paginating twice in same case to make sure data is not being omdified
98
99
  it 'should paginate paginated collections' do
99
100
  products = @adapter.get('/collections/catalog/products', :page => 2, :per_page => 2)
100
101
  products[:page].should == 2
@@ -103,6 +104,13 @@ describe ApiBee::Adapters::Hash do
103
104
  products[:entries].size.should == 2
104
105
  products[:entries][0][:title].should == 'Foo 3'
105
106
  products[:entries][1][:title].should == 'Foo 4'
107
+
108
+ products = @adapter.get('/collections/catalog/products', :page => 3, :per_page => 2)
109
+ products[:page].should == 3
110
+ products[:total_entries].should == 5
111
+ products[:per_page].should == 2
112
+ products[:entries].size.should == 1
113
+ products[:entries][0][:title].should == 'Foo 5'
106
114
  end
107
115
 
108
116
  it 'should paginate last page correctly' do
@@ -117,17 +117,40 @@ describe ApiBee do
117
117
  end
118
118
  end
119
119
 
120
- describe '#[]=' do
120
+ context 'hash methods' do
121
121
 
122
122
  before do
123
123
  @adapter = mock('Adapter')
124
124
  @config = ApiBee.new_config
125
+ @node = ApiBee::Node.resolve(@adapter, @config, {:title => 'Blah', :href => '/products/1', :empty_field => nil})
125
126
  end
126
127
 
127
- it 'should set attributes in nodes' do
128
- node = ApiBee::Node.resolve(@adapter, @config, {:title => 'Blah', :total_entries => 4, :href => '/products'})
129
- node[:foo] = 11
130
- node[:foo].should == 11
128
+ describe '#[]=' do
129
+ it 'should set attributes in nodes' do
130
+ @node[:foo] = 11
131
+ @node[:foo].should == 11
132
+ end
133
+ end
134
+
135
+ describe '#has_key?' do
136
+ it 'should delegate to local attributes' do
137
+ @adapter.should_not_receive(:get)
138
+ @node.has_key?(:title).should be_true
139
+ end
140
+
141
+ it 'should fetch from :href if available' do
142
+ @adapter.should_receive(:get).with('/products/1').and_return(:age => 2)
143
+ @node.has_key?(:age).should be_true
144
+ end
145
+
146
+ it 'should return true for nil value' do
147
+ @node.has_key?(:empty_field).should be_true
148
+ end
149
+
150
+ it 'should return false for missing keys' do
151
+ @adapter.should_receive(:get).with('/products/1').and_return(:age => 2)
152
+ @node.has_key?(:missing_field).should be_false
153
+ end
131
154
  end
132
155
  end
133
156
 
@@ -0,0 +1,132 @@
1
+ require 'spec_helper'
2
+
3
+ describe ApiBee do
4
+
5
+ before do
6
+
7
+ class BookWrapper
8
+ attr_accessor :node
9
+ def initialize(node)
10
+ @node = node
11
+ end
12
+
13
+ def title_and_isbn
14
+ [@node[:title], @node[:isbn]].join ' '
15
+ end
16
+
17
+ def author
18
+ @node[:author]
19
+ end
20
+
21
+ def genre
22
+ @node[:genre]
23
+ end
24
+ end
25
+
26
+ class AuthorWrapper < BookWrapper
27
+ def name
28
+ @node[:name]
29
+ end
30
+ end
31
+
32
+ @adapter = Class.new do
33
+
34
+ def get(*args)
35
+ # will be mocked
36
+ end
37
+
38
+ def wrap(node, name)
39
+ case name
40
+ when /book/
41
+ BookWrapper.new node
42
+ when /author/
43
+ AuthorWrapper.new node
44
+ else
45
+ node
46
+ end
47
+ end
48
+ end
49
+
50
+ @data = {
51
+ :total_entries => 5,
52
+ :page => 1,
53
+ :per_page => 2,
54
+ :href => '/books',
55
+ :entries => [
56
+ {
57
+ :title => 'Heart of Darkness',
58
+ :isbn => 123,
59
+ :author => {
60
+ :name => 'J Conrad'
61
+ },
62
+ :genre => {
63
+ :name => 'fiction'
64
+ }
65
+ },
66
+ {
67
+ :title => 'Rayuela',
68
+ :isbn => 124,
69
+ :author => {
70
+ :name => 'Julio Cortazar'
71
+ },
72
+ :genre => {
73
+ :name => 'fiction'
74
+ }
75
+ }
76
+ ]
77
+ }
78
+
79
+ @adapter_instance = @adapter.new
80
+
81
+ @adapter.stub!(:new).and_return @adapter_instance
82
+
83
+ @adapter_instance.stub!(:get).with('/books', {}).and_return @data
84
+
85
+ @adapter_instance.stub!(:get).with('/books/123', {}).and_return @data[:entries].first
86
+
87
+ @api = ApiBee.setup(@adapter)
88
+ end
89
+
90
+ it 'should return a proxy' do
91
+ @api.get('/books').should be_kind_of(ApiBee::Proxy)
92
+ end
93
+
94
+ context 'iterating' do
95
+ it 'should wrap first level nodes' do
96
+ @api.get('/books').map do |book|
97
+ book.class
98
+ end.should == [BookWrapper, BookWrapper]
99
+ end
100
+
101
+ it 'should initialize wrappers with node as argument' do
102
+ @api.get('/books').map do |book|
103
+ book.title_and_isbn
104
+ end.should == ["Heart of Darkness 123", "Rayuela 124"]
105
+ end
106
+
107
+ it 'should wrap nested nodes' do
108
+ @api.get('/books').map do |book|
109
+ book.author.class
110
+ end.should == [AuthorWrapper, AuthorWrapper]
111
+ end
112
+
113
+ it 'should initialize nested wrappers with node as argument' do
114
+ @api.get('/books').map do |book|
115
+ book.author.name
116
+ end.should == ['J Conrad', 'Julio Cortazar']
117
+ end
118
+
119
+ it 'should NOT wrap nested nodes if no wrapper defined' do
120
+ @api.get('/books').map do |book|
121
+ book.genre.class
122
+ end.should == [ApiBee::Node::Single, ApiBee::Node::Single]
123
+ end
124
+ end
125
+
126
+ context 'accesing single nodes' do
127
+ it 'should wrap them' do
128
+ @api.get('/books/123').title_and_isbn.should == 'Heart of Darkness 123'
129
+ end
130
+ end
131
+
132
+ 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.1.7
5
+ version: 0.1.8
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: 2012-01-18 00:00:00 Z
13
+ date: 2012-03-01 00:00:00 Z
14
14
  dependencies: []
15
15
 
16
16
  description: Small Ruby client for discoverable, lazily-loaded, paginated JSON APIs
@@ -41,6 +41,7 @@ files:
41
41
  - spec/node_spec.rb
42
42
  - spec/setup_spec.rb
43
43
  - spec/spec_helper.rb
44
+ - spec/wrapper_spec.rb
44
45
  homepage: https://github.com/ismasan/ApiBee
45
46
  licenses: []
46
47
 
@@ -64,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
65
  requirements: []
65
66
 
66
67
  rubyforge_project: api_bee
67
- rubygems_version: 1.8.6
68
+ rubygems_version: 1.8.17
68
69
  signing_key:
69
70
  specification_version: 3
70
71
  summary: API Bee is a small client / spec for a particular style of JSON API. Use Hash adapter for local data access.
@@ -73,3 +74,4 @@ test_files:
73
74
  - spec/node_spec.rb
74
75
  - spec/setup_spec.rb
75
76
  - spec/spec_helper.rb
77
+ - spec/wrapper_spec.rb