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 +1 -0
- data/README.mkd +37 -0
- data/examples/catalog.yml +12 -0
- data/examples/github_api.rb +24 -1
- data/examples/yaml_catalog.rb +55 -2
- data/lib/api_bee/adapters/hash.rb +8 -1
- data/lib/api_bee/node.rb +20 -8
- data/lib/api_bee/version.rb +1 -1
- data/spec/hash_adapter_spec.rb +8 -0
- data/spec/node_spec.rb +28 -5
- data/spec/wrapper_spec.rb +132 -0
- metadata +5 -3
data/Gemfile
CHANGED
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.
|
data/examples/catalog.yml
CHANGED
@@ -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
|
data/examples/github_api.rb
CHANGED
@@ -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]
|
139
|
+
puts "An owner is #{one[:owner].login}, who has #{one[:owner].public_repos} public repos"
|
data/examples/yaml_catalog.rb
CHANGED
@@ -2,9 +2,59 @@ $LOAD_PATH.unshift '../lib'
|
|
2
2
|
require 'yaml'
|
3
3
|
require 'api_bee'
|
4
4
|
|
5
|
-
|
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
|
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
|
data/lib/api_bee/node.rb
CHANGED
@@ -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,
|
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,
|
35
|
+
List.new adapter, config, attrs, attribute_name
|
36
36
|
else
|
37
|
-
Single.new adapter, config, attrs,
|
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 ||=
|
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
|
data/lib/api_bee/version.rb
CHANGED
data/spec/hash_adapter_spec.rb
CHANGED
@@ -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
|
data/spec/node_spec.rb
CHANGED
@@ -117,17 +117,40 @@ describe ApiBee do
|
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
120
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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.
|
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
|
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.
|
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
|