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