api_bee 0.1.7 → 0.1.8

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