api_bee 0.0.1
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/.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
|