api_bee 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/Gemfile +9 -0
- data/README.mkd +119 -0
- data/Rakefile +8 -0
- data/api_bee.gemspec +21 -0
- data/examples/catalog.yml +61 -0
- data/examples/yaml_catalog.rb +23 -0
- data/lib/api_bee/adapters/hash.rb +72 -0
- data/lib/api_bee/node.rb +140 -0
- data/lib/api_bee/proxy.rb +17 -0
- data/lib/api_bee/version.rb +3 -0
- data/lib/api_bee.rb +39 -0
- data/spec/hash_adapter_spec.rb +96 -0
- data/spec/node_spec.rb +168 -0
- data/spec/setup_spec.rb +60 -0
- data/spec/spec_helper.rb +16 -0
- metadata +74 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.mkd
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# API BEE
|
2
|
+
|
3
|
+
API Bee is a small client / spec for a particular style of JSON API.
|
4
|
+
|
5
|
+
These APIs must
|
6
|
+
|
7
|
+
* Expose resource collections as paginated lists, with entries and paging properties.
|
8
|
+
* Resource entities in collections must include an 'href' property pointing to the individual resource, so as to provide a degree of discoverability.
|
9
|
+
|
10
|
+
A single resource might look like:
|
11
|
+
|
12
|
+
{
|
13
|
+
'name': 'Foo',
|
14
|
+
'description': 'foo resoruce',
|
15
|
+
'href': 'http://api.myservice.com/resources/foo'
|
16
|
+
}
|
17
|
+
|
18
|
+
A resource collection looks like:
|
19
|
+
|
20
|
+
{
|
21
|
+
'href': 'http://api.myservice.com/resources',
|
22
|
+
'total_entries': 100,
|
23
|
+
'page': 1,
|
24
|
+
'per_page': 10,
|
25
|
+
'entries': [
|
26
|
+
{
|
27
|
+
'name': 'Foo',
|
28
|
+
'description': 'foo resoruce',
|
29
|
+
'href': 'http://api.myservice.com/resources/foo'
|
30
|
+
},
|
31
|
+
{
|
32
|
+
'name': 'Bar',
|
33
|
+
'description': 'bar resoruce',
|
34
|
+
'href': 'http://api.myservice.com/resources/bar'
|
35
|
+
},
|
36
|
+
...
|
37
|
+
]
|
38
|
+
}
|
39
|
+
|
40
|
+
Collection resources must include the fields 'href', 'total_entries', 'page', 'per_page' and 'entries'. This allows clients to paginate and fetch more pages.
|
41
|
+
|
42
|
+
## Adapters
|
43
|
+
|
44
|
+
It is up to individual adapters to talk to different services, handle auth, etc. An adapter must at least implement 'get' for read-only APIs.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
class ApiBee::Adapters::Special
|
48
|
+
def get(path, options = {})
|
49
|
+
# fetch JSON from remote API, passing pagination options if available
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
The use it:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
api = ApiBee.setup :special, optional_custom_data
|
58
|
+
|
59
|
+
api.get('/my/resources').each do |r|
|
60
|
+
r[:name]
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
## Lazy loading
|
65
|
+
|
66
|
+
ApiBee wraps your adapters in lazy-loading objects. API calls will only be issued when accessing or iterating data.
|
67
|
+
|
68
|
+
The 'href' property in entities will be used to load more data. For example:
|
69
|
+
|
70
|
+
# /resources
|
71
|
+
[
|
72
|
+
{
|
73
|
+
'title': 'Foo bar',
|
74
|
+
'href': '/resources/foo-bar'
|
75
|
+
}
|
76
|
+
]
|
77
|
+
|
78
|
+
# /resources/foo-bar
|
79
|
+
{
|
80
|
+
'title': 'Foo bar',
|
81
|
+
'description': 'Foo description'
|
82
|
+
}
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
api = ApiBee.get(:some_adapter)
|
86
|
+
|
87
|
+
resources = api.get('/resources') # no API call is made
|
88
|
+
|
89
|
+
resource = resources.first # call to /resources is made
|
90
|
+
|
91
|
+
resource['title'] # => 'Foo bar', title data available
|
92
|
+
|
93
|
+
resource['description'] # => 'Foo description'. Makes internal new request to /resources/foo-bar
|
94
|
+
```
|
95
|
+
|
96
|
+
|
97
|
+
## Hash adapter
|
98
|
+
|
99
|
+
ApiBee ships with an in-memory Hash adapter so it can be use with test/local data (for example a YAML file).
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
api = ApiBee.setup(:hash, YAML.load_file('./my_data.yml'))
|
103
|
+
|
104
|
+
products = api.get('/products') # => ApiBee::Node::List
|
105
|
+
|
106
|
+
products.first # => ApiBee::Node::Single
|
107
|
+
|
108
|
+
products.each() # iterate current page
|
109
|
+
|
110
|
+
products.current_page # => 1
|
111
|
+
|
112
|
+
products.paginate(:page => 2, :per_page => 4) # => ApiBee::Node::List # Next page
|
113
|
+
|
114
|
+
products.has_next_page? # => false
|
115
|
+
|
116
|
+
products.has_prev_page? # => true
|
117
|
+
|
118
|
+
products.prev_page # => 1
|
119
|
+
```
|
data/Rakefile
ADDED
data/api_bee.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "api_bee/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "api_bee"
|
7
|
+
s.version = ApiBee::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ismael Celis"]
|
10
|
+
s.email = ["ismaelct@gmail.com"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Small Ruby client for discoverable, paginated JSON APIs}
|
13
|
+
s.description = %q{API Bee is a small client / spec for a particular style of JSON API. USe Hash adapter for local data access.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "api_bee"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
:products:
|
2
|
+
:total_entries: 6
|
3
|
+
:href: /products
|
4
|
+
:entries:
|
5
|
+
-
|
6
|
+
:title: P1
|
7
|
+
:id: p1
|
8
|
+
:price: 100
|
9
|
+
-
|
10
|
+
:title: P2
|
11
|
+
:id: p2
|
12
|
+
:price: 200
|
13
|
+
-
|
14
|
+
:title: P3
|
15
|
+
:id: p3
|
16
|
+
:price: 300
|
17
|
+
-
|
18
|
+
:title: P4
|
19
|
+
:id: p4
|
20
|
+
:price: 400
|
21
|
+
-
|
22
|
+
:title: P5
|
23
|
+
:id: p5
|
24
|
+
:price: 500
|
25
|
+
-
|
26
|
+
:title: P6
|
27
|
+
:id: p6
|
28
|
+
:price: 600
|
29
|
+
|
30
|
+
:collections:
|
31
|
+
:total_entries: 2
|
32
|
+
:href: /collections
|
33
|
+
:entries:
|
34
|
+
-
|
35
|
+
:title: Collection 1
|
36
|
+
:id: c1
|
37
|
+
:products:
|
38
|
+
:total_entries: 2
|
39
|
+
:page: 1
|
40
|
+
:per_page: 1
|
41
|
+
:href: /products
|
42
|
+
:entries:
|
43
|
+
-
|
44
|
+
:href: /products/p1
|
45
|
+
-
|
46
|
+
:href: /products/p2
|
47
|
+
|
48
|
+
-
|
49
|
+
:title: Collection 2
|
50
|
+
:id: c2
|
51
|
+
:products:
|
52
|
+
:total_entries: 2
|
53
|
+
:page: 1
|
54
|
+
:per_page: 1
|
55
|
+
:href: /products
|
56
|
+
:entries:
|
57
|
+
-
|
58
|
+
:href: /products/p1
|
59
|
+
-
|
60
|
+
:href: /products/p5
|
61
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$LOAD_PATH.unshift '../lib'
|
2
|
+
require 'yaml'
|
3
|
+
require 'api_bee'
|
4
|
+
|
5
|
+
api = ApiBee.setup(:hash, YAML.load_file('./catalog.yml'))
|
6
|
+
|
7
|
+
collection = api.get('/collections/c1')
|
8
|
+
|
9
|
+
p collection[:title]
|
10
|
+
|
11
|
+
products = collection[:products]
|
12
|
+
|
13
|
+
puts "First page"
|
14
|
+
|
15
|
+
products.each do |p|
|
16
|
+
p p[:title]
|
17
|
+
end
|
18
|
+
|
19
|
+
puts "Second page"
|
20
|
+
|
21
|
+
products.paginate(:page => products.next_page, :per_page => 2).each do |p|
|
22
|
+
p p[:title]
|
23
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ApiBee
|
2
|
+
|
3
|
+
module Adapters
|
4
|
+
|
5
|
+
class Hash
|
6
|
+
|
7
|
+
attr_reader :data
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
@data = args.last
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(href, opts = {})
|
14
|
+
segments = parse_href(href)
|
15
|
+
found = segments.inject(data) do |mem,i|
|
16
|
+
case mem
|
17
|
+
when ::Hash
|
18
|
+
handle_hash_data mem, i, opts
|
19
|
+
when ::Array
|
20
|
+
handle_array_data mem, i
|
21
|
+
else
|
22
|
+
mem
|
23
|
+
end
|
24
|
+
end
|
25
|
+
found
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def parse_href(href)
|
31
|
+
href.gsub(/^\//, '').split('/')
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle_hash_data(hash, key, opts = {})
|
35
|
+
if is_paginated?(hash) # paginated collection
|
36
|
+
handle_array_data hash[:entries], key
|
37
|
+
else # /products. Might be a paginated list
|
38
|
+
r = hash[key.to_sym]
|
39
|
+
if opts.keys.include?(:page) && opts.keys.include?(:per_page) && r.kind_of?(::Hash) && is_paginated?(r)
|
40
|
+
paginate(r, opts[:page], opts[:per_page])
|
41
|
+
else
|
42
|
+
r
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def handle_array_data(array, key)
|
48
|
+
if array[0].kind_of?(::Hash)
|
49
|
+
array.find {|e| e[:id] == key}
|
50
|
+
else
|
51
|
+
array
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def is_paginated?(hash)
|
56
|
+
hash[ApiBee.config.uri_property_name] && hash[:total_entries]
|
57
|
+
end
|
58
|
+
|
59
|
+
def paginate(list, page, per_page)
|
60
|
+
from = page * per_page - per_page
|
61
|
+
to = page * per_page
|
62
|
+
list[:entries] = list[:entries].to_a[from...to]
|
63
|
+
list[:current_page] = page
|
64
|
+
list[:per_page] = per_page
|
65
|
+
list
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
data/lib/api_bee/node.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
module ApiBee
|
2
|
+
|
3
|
+
class Node
|
4
|
+
|
5
|
+
def self.resolve(adapter, attrs)
|
6
|
+
keys = attrs.keys.map{|k| k.to_sym}
|
7
|
+
if keys.include?(:total_entries) && keys.include?(ApiBee.config.uri_property_name) # is a paginator
|
8
|
+
List.new adapter, attrs
|
9
|
+
else
|
10
|
+
Single.new adapter, attrs
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :adapter
|
15
|
+
|
16
|
+
def initialize(adapter, attrs)
|
17
|
+
@adapter = adapter
|
18
|
+
@attributes = {}
|
19
|
+
update_attributes attrs
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](attribute_name)
|
23
|
+
if value = @attributes[attribute_name]
|
24
|
+
resolve_values_to_nodes value
|
25
|
+
elsif has_more? # check whether there's more info in API
|
26
|
+
load_more!
|
27
|
+
self[attribute_name] # recurse once
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def update_attributes(attrs)
|
36
|
+
@attributes.merge!(attrs)
|
37
|
+
end
|
38
|
+
|
39
|
+
def resolve_values_to_nodes(value)
|
40
|
+
case value
|
41
|
+
when ::Hash
|
42
|
+
Node.resolve @adapter, value
|
43
|
+
when ::Array
|
44
|
+
value.map {|v| resolve_values_to_nodes(v)} # recurse
|
45
|
+
else
|
46
|
+
value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_more?
|
51
|
+
!@complete && @attributes[ApiBee.config.uri_property_name]
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_more!
|
55
|
+
more_data = @adapter.get(@attributes[ApiBee.config.uri_property_name])
|
56
|
+
update_attributes more_data if more_data
|
57
|
+
@complete = true
|
58
|
+
end
|
59
|
+
|
60
|
+
class Single < Node
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
class List < Node
|
65
|
+
|
66
|
+
DEFAULT_PER_PAGE = 100
|
67
|
+
|
68
|
+
def total_entries
|
69
|
+
@attributes[:total_entries].to_i
|
70
|
+
end
|
71
|
+
|
72
|
+
def size
|
73
|
+
entries.size
|
74
|
+
end
|
75
|
+
|
76
|
+
def current_page
|
77
|
+
(@attributes[:current_page] || 1).to_i
|
78
|
+
end
|
79
|
+
|
80
|
+
def per_page
|
81
|
+
(@attributes[:per_page] || DEFAULT_PER_PAGE).to_i
|
82
|
+
end
|
83
|
+
|
84
|
+
def total_pages
|
85
|
+
div = total_entries / per_page
|
86
|
+
div < 1 ? 1 : div
|
87
|
+
end
|
88
|
+
|
89
|
+
def next_page
|
90
|
+
current_page + 1
|
91
|
+
end
|
92
|
+
|
93
|
+
def prev_page
|
94
|
+
current_page - 1
|
95
|
+
end
|
96
|
+
|
97
|
+
def pages
|
98
|
+
(1..total_pages).to_a
|
99
|
+
end
|
100
|
+
|
101
|
+
def has_next_page?
|
102
|
+
next_page <= total_pages
|
103
|
+
end
|
104
|
+
|
105
|
+
def has_prev_page?
|
106
|
+
current_page > 1
|
107
|
+
end
|
108
|
+
|
109
|
+
def first
|
110
|
+
entries.first
|
111
|
+
end
|
112
|
+
|
113
|
+
def last
|
114
|
+
entries.last
|
115
|
+
end
|
116
|
+
|
117
|
+
def each(&block)
|
118
|
+
entries.each(&block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def each_with_index(&block)
|
122
|
+
entries.each_with_index(&block)
|
123
|
+
end
|
124
|
+
|
125
|
+
def paginate(options = {})
|
126
|
+
data = @adapter.get(@attributes[ApiBee.config.uri_property_name], options)
|
127
|
+
Node.resolve @adapter, data
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
def entries
|
133
|
+
@entries ||= (self[:entries] || [])
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
data/lib/api_bee.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
module ApiBee
|
3
|
+
|
4
|
+
class << ApiBee
|
5
|
+
attr_reader :config
|
6
|
+
|
7
|
+
def setup(adapter_klass, *args)
|
8
|
+
|
9
|
+
yield config if block_given?
|
10
|
+
|
11
|
+
adapter = if adapter_klass.is_a?(::Symbol)
|
12
|
+
require File.join('api_bee', 'adapters', adapter_klass.to_s)
|
13
|
+
klass = adapter_klass.to_s.gsub(/(^.{1})/){$1.upcase}
|
14
|
+
Adapters.const_get(klass).new(*args)
|
15
|
+
else
|
16
|
+
adapter_klass.new *args
|
17
|
+
end
|
18
|
+
|
19
|
+
raise NoMethodError, "Adapter must implement #get(path, *args) method" unless adapter.respond_to?(:get)
|
20
|
+
Proxy.new adapter
|
21
|
+
end
|
22
|
+
|
23
|
+
def config
|
24
|
+
@config ||= OpenStruct.new
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
# Defaults
|
30
|
+
self.config.uri_property_name = :href
|
31
|
+
|
32
|
+
module Adapters
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
require 'api_bee/proxy'
|
39
|
+
require 'api_bee/node'
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'api_bee/adapters/hash'
|
3
|
+
|
4
|
+
describe ApiBee::Adapters::Hash do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@data = {
|
8
|
+
# Collections
|
9
|
+
:collections => [
|
10
|
+
{
|
11
|
+
:title => 'Catalog',
|
12
|
+
:id => 'catalog',
|
13
|
+
:foos => [1,2,3,4],
|
14
|
+
:products => {
|
15
|
+
:total_entries => 4,
|
16
|
+
:current_page => 1,
|
17
|
+
:per_page => 4,
|
18
|
+
:href => '/collections/catalog/products',
|
19
|
+
:entries => [
|
20
|
+
{
|
21
|
+
:id => 'foo-1',
|
22
|
+
:href => '/products/foo-1'
|
23
|
+
},
|
24
|
+
{
|
25
|
+
:id => 'foo-2',
|
26
|
+
:title => 'Foo 2',
|
27
|
+
:href => '/products/foo-2'
|
28
|
+
},
|
29
|
+
{
|
30
|
+
:id => 'foo-3',
|
31
|
+
:title => 'Foo 3',
|
32
|
+
:href => '/products/foo-3'
|
33
|
+
},
|
34
|
+
{
|
35
|
+
:id => 'foo-4',
|
36
|
+
:title => 'Foo 4',
|
37
|
+
:href => '/products/foo-4'
|
38
|
+
}
|
39
|
+
]
|
40
|
+
}
|
41
|
+
}
|
42
|
+
]
|
43
|
+
}
|
44
|
+
|
45
|
+
@adapter = ApiBee::Adapters::Hash.new(@data)
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'accessing single nodes' do
|
49
|
+
|
50
|
+
before do
|
51
|
+
@collection = @adapter.get('/collections/catalog')
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should find node by :id' do
|
55
|
+
@collection[:title].should == 'Catalog'
|
56
|
+
@collection[:id].should == 'catalog'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should find entries in paginated collections' do
|
60
|
+
product = @adapter.get('/collections/catalog/products/foo-2')
|
61
|
+
product[:title].should == 'Foo 2'
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should return whole collection if no pagination' do
|
65
|
+
products = @adapter.get('/collections/catalog/products')
|
66
|
+
# products.should == 1
|
67
|
+
products[:current_page].should == 1
|
68
|
+
products[:total_entries].should == 4
|
69
|
+
products[:per_page].should == 4
|
70
|
+
products[:entries].size.should == 4
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should paginate paginated collections' do
|
74
|
+
products = @adapter.get('/collections/catalog/products', :page => 2, :per_page => 2)
|
75
|
+
products[:current_page].should == 2
|
76
|
+
products[:total_entries].should == 4
|
77
|
+
products[:per_page].should == 2
|
78
|
+
products[:entries].size.should == 2
|
79
|
+
products[:entries][0][:title].should == 'Foo 3'
|
80
|
+
products[:entries][1][:title].should == 'Foo 4'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'accessing node collections' do
|
85
|
+
|
86
|
+
before do
|
87
|
+
@list = @adapter.get('/collections/catalog/foos')
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should return array' do
|
91
|
+
@list.size.should == 4
|
92
|
+
@list.should == [1,2,3,4]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
data/spec/node_spec.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ApiBee do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@data = {
|
7
|
+
# Products
|
8
|
+
:products => {
|
9
|
+
:href => '/products',
|
10
|
+
:total_entries => 6,
|
11
|
+
:entries => [
|
12
|
+
{
|
13
|
+
:title => 'Foo 1',
|
14
|
+
:id => 'foo-1',
|
15
|
+
:price => 100,
|
16
|
+
:description => 'Foo 1 desc'
|
17
|
+
},
|
18
|
+
{
|
19
|
+
:title => 'Foo 2',
|
20
|
+
:id => 'foo-2',
|
21
|
+
:price => 200,
|
22
|
+
:description => 'Foo 2 desc'
|
23
|
+
},
|
24
|
+
{
|
25
|
+
:title => 'Foo 3',
|
26
|
+
:id => 'foo-3',
|
27
|
+
:price => 300,
|
28
|
+
:description => 'Foo 3 desc'
|
29
|
+
},
|
30
|
+
{
|
31
|
+
:title => 'Foo 4',
|
32
|
+
:id => 'foo-4',
|
33
|
+
:price => 400,
|
34
|
+
:description => 'Foo 4 desc'
|
35
|
+
},
|
36
|
+
{
|
37
|
+
:title => 'Foo 5',
|
38
|
+
:id => 'foo-5',
|
39
|
+
:price => 500,
|
40
|
+
:description => 'Foo 5 desc'
|
41
|
+
},
|
42
|
+
{
|
43
|
+
:title => 'Foo 6',
|
44
|
+
:id => 'foo-6',
|
45
|
+
:price => 600,
|
46
|
+
:description => 'Foo 6 desc'
|
47
|
+
}
|
48
|
+
]
|
49
|
+
},
|
50
|
+
# Collections
|
51
|
+
:collections => [
|
52
|
+
{
|
53
|
+
:title => 'Catalog',
|
54
|
+
:id => 'catalog',
|
55
|
+
:products => {
|
56
|
+
:href => '/products',
|
57
|
+
:total_entries => 4,
|
58
|
+
:current_page => 1,
|
59
|
+
:per_page => 2,
|
60
|
+
:href => '/products',
|
61
|
+
:entries => [
|
62
|
+
{
|
63
|
+
:id => 'foo-1',
|
64
|
+
:href => '/products/foo-1'
|
65
|
+
},
|
66
|
+
{
|
67
|
+
:id => 'foo-2',
|
68
|
+
:title => 'Foo 2',
|
69
|
+
:href => '/products/foo-2'
|
70
|
+
}
|
71
|
+
]
|
72
|
+
}
|
73
|
+
}
|
74
|
+
]
|
75
|
+
}
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '.resolve' do
|
80
|
+
|
81
|
+
before do
|
82
|
+
@adapter = mock('Adapter')
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should resolve single nodes' do
|
86
|
+
node = ApiBee::Node.resolve(@adapter, {:title => 'Blah', :foo => [1,2,3]})
|
87
|
+
node[:title].should == 'Blah'
|
88
|
+
node.adapter.should == @adapter
|
89
|
+
node.should be_kind_of(ApiBee::Node::Single)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should resolve paginated lists' do
|
93
|
+
node = ApiBee::Node.resolve(@adapter, {:title => 'Blah', :total_entries => 4, :href => '/products'})
|
94
|
+
node.total_entries.should == 4
|
95
|
+
node.adapter.should == @adapter
|
96
|
+
node.should be_kind_of(ApiBee::Node::List)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'lazy loading' do
|
101
|
+
before do
|
102
|
+
require 'api_bee/adapters/hash'
|
103
|
+
@adapter = ApiBee::Adapters::Hash.new(@data)
|
104
|
+
ApiBee::Adapters::Hash.should_receive(:new).with(@data).and_return @adapter
|
105
|
+
@api = ApiBee.setup(:hash, @data)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should return a Proxy' do
|
109
|
+
@api.should be_kind_of(ApiBee::Proxy)
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'single nodes' do
|
113
|
+
it 'should call adapter only when accessing needed attributes' do
|
114
|
+
hash = @data[:collections].last
|
115
|
+
@adapter.should_receive(:get).exactly(1).times.with('/collections/catalog').and_return hash
|
116
|
+
node = @api.get('/collections/catalog')
|
117
|
+
node[:title].should == 'Catalog'
|
118
|
+
node[:id].should == 'catalog'
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe 'paginated lists' do
|
123
|
+
before do
|
124
|
+
@collection = @api.get('/collections/catalog')
|
125
|
+
@products = @collection[:products]
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should have a paginator interface' do
|
129
|
+
@products.total_entries.should == 4
|
130
|
+
@products.size.should == 2
|
131
|
+
@products.total_pages.should == 2
|
132
|
+
@products.current_page.should == 1
|
133
|
+
@products.pages.should == [1,2]
|
134
|
+
@products.has_next_page?.should be_true
|
135
|
+
@products.has_prev_page?.should be_false
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should iterate the first page' do
|
139
|
+
titles = []
|
140
|
+
klasses = []
|
141
|
+
@products.each {|p| titles << p[:title]}
|
142
|
+
@products.each {|p| klasses << p.class}
|
143
|
+
klasses.should == [ApiBee::Node::Single, ApiBee::Node::Single]
|
144
|
+
titles.should == ['Foo 1', 'Foo 2']
|
145
|
+
@products.first[:title].should == 'Foo 1'
|
146
|
+
@products.last[:title].should == 'Foo 2'
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'should navigate to the second page' do
|
150
|
+
@products = @products.paginate(:page => 2, :per_page => 2)
|
151
|
+
titles = []
|
152
|
+
klasses = []
|
153
|
+
@products.each {|p| titles << p[:title]}
|
154
|
+
@products.each {|p| klasses << p.class}
|
155
|
+
@products.current_page.should == 2
|
156
|
+
@products.total_entries.should == 6
|
157
|
+
@products.size.should == 2
|
158
|
+
klasses.should == [ApiBee::Node::Single, ApiBee::Node::Single]
|
159
|
+
titles.should == ['Foo 3', 'Foo 4']
|
160
|
+
@products.first[:title].should == 'Foo 3'
|
161
|
+
@products.last[:title].should == 'Foo 4'
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
data/spec/setup_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'ApiBee.setup' do
|
4
|
+
|
5
|
+
describe 'with bundled adapter' do
|
6
|
+
|
7
|
+
it 'should return a proxy to instantiated adapter' do
|
8
|
+
api = ApiBee.setup(:hash, {})
|
9
|
+
api.adapter.should be_kind_of(ApiBee::Adapters::Hash)
|
10
|
+
end
|
11
|
+
end
|
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
|
+
describe 'with custom adapter class' do
|
31
|
+
before do
|
32
|
+
|
33
|
+
class CustomAdapter
|
34
|
+
attr_reader :opts
|
35
|
+
def initialize(opts)
|
36
|
+
@opts = opts
|
37
|
+
end
|
38
|
+
|
39
|
+
def get(path, *args);end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'shoud instantiate adapter with options' do
|
45
|
+
api = ApiBee.setup(CustomAdapter, :one => 1)
|
46
|
+
api.adapter.should be_kind_of(CustomAdapter)
|
47
|
+
api.adapter.opts.should == {:one => 1}
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'with adapter without #get method' do
|
53
|
+
it 'should complain' do
|
54
|
+
lambda {
|
55
|
+
ApiBee.setup(String)
|
56
|
+
}.should raise_error("Adapter must implement #get(path, *args) method")
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
$TESTING = true
|
5
|
+
|
6
|
+
Bundler.setup(:default, :test)
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib'))
|
9
|
+
|
10
|
+
ENV['RACK_ENV'] = 'test'
|
11
|
+
|
12
|
+
require 'api_bee'
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
# some (optional) config here
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_bee
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ismael Celis
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-09-19 00:00:00 Z
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: API Bee is a small client / spec for a particular style of JSON API. USe Hash adapter for local data access.
|
17
|
+
email:
|
18
|
+
- ismaelct@gmail.com
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- .rspec
|
28
|
+
- Gemfile
|
29
|
+
- README.mkd
|
30
|
+
- Rakefile
|
31
|
+
- api_bee.gemspec
|
32
|
+
- examples/catalog.yml
|
33
|
+
- examples/yaml_catalog.rb
|
34
|
+
- lib/api_bee.rb
|
35
|
+
- lib/api_bee/adapters/hash.rb
|
36
|
+
- lib/api_bee/node.rb
|
37
|
+
- lib/api_bee/proxy.rb
|
38
|
+
- lib/api_bee/version.rb
|
39
|
+
- spec/hash_adapter_spec.rb
|
40
|
+
- spec/node_spec.rb
|
41
|
+
- spec/setup_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
homepage: ""
|
44
|
+
licenses: []
|
45
|
+
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project: api_bee
|
66
|
+
rubygems_version: 1.8.6
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: Small Ruby client for discoverable, paginated JSON APIs
|
70
|
+
test_files:
|
71
|
+
- spec/hash_adapter_spec.rb
|
72
|
+
- spec/node_spec.rb
|
73
|
+
- spec/setup_spec.rb
|
74
|
+
- spec/spec_helper.rb
|