apitizer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +3 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +91 -0
- data/Rakefile +7 -0
- data/apitizer.gemspec +32 -0
- data/lib/apitizer.rb +14 -0
- data/lib/apitizer/base.rb +61 -0
- data/lib/apitizer/connection.rb +10 -0
- data/lib/apitizer/connection/adaptor.rb +13 -0
- data/lib/apitizer/connection/adaptor/standard.rb +34 -0
- data/lib/apitizer/connection/dispatcher.rb +24 -0
- data/lib/apitizer/connection/request.rb +16 -0
- data/lib/apitizer/connection/response.rb +12 -0
- data/lib/apitizer/core.rb +24 -0
- data/lib/apitizer/helper.rb +46 -0
- data/lib/apitizer/processing.rb +8 -0
- data/lib/apitizer/processing/parser.rb +14 -0
- data/lib/apitizer/processing/parser/json.rb +15 -0
- data/lib/apitizer/processing/parser/yaml.rb +15 -0
- data/lib/apitizer/processing/translator.rb +13 -0
- data/lib/apitizer/result.rb +15 -0
- data/lib/apitizer/routing.rb +10 -0
- data/lib/apitizer/routing/mapper.rb +47 -0
- data/lib/apitizer/routing/node.rb +5 -0
- data/lib/apitizer/routing/node/base.rb +44 -0
- data/lib/apitizer/routing/node/collection.rb +34 -0
- data/lib/apitizer/routing/node/operation.rb +32 -0
- data/lib/apitizer/routing/node/root.rb +15 -0
- data/lib/apitizer/routing/node/scope.rb +19 -0
- data/lib/apitizer/routing/path.rb +26 -0
- data/lib/apitizer/routing/proxy.rb +17 -0
- data/lib/apitizer/version.rb +3 -0
- data/spec/apitizer/base_spec.rb +71 -0
- data/spec/apitizer/connection/adaptor_spec.rb +24 -0
- data/spec/apitizer/connection/dispatcher_spec.rb +39 -0
- data/spec/apitizer/helper_spec.rb +87 -0
- data/spec/apitizer/processing/parser_spec.rb +23 -0
- data/spec/apitizer/result_spec.rb +19 -0
- data/spec/apitizer/routing/mapper_spec.rb +80 -0
- data/spec/apitizer/routing/node_spec.rb +63 -0
- data/spec/apitizer/routing/path_spec.rb +102 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/factory_helper.rb +23 -0
- data/spec/support/resource_helper.rb +18 -0
- metadata +203 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Connection::Dispatcher do
|
4
|
+
extend ResourceHelper
|
5
|
+
include ResourceHelper
|
6
|
+
|
7
|
+
let(:headers) { { 'Secret-Token' => 'arbitrary' } }
|
8
|
+
let(:address) { 'https://service.com/api/v1/json/articles' }
|
9
|
+
let(:subject) do
|
10
|
+
Apitizer::Connection::Dispatcher.new(
|
11
|
+
dictionary: rest_http_dictionary, headers: headers)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_request(action, address)
|
15
|
+
double(action: action, address: address, parameters: {})
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#process' do
|
19
|
+
restful_actions.each do |action|
|
20
|
+
method = rest_http_dictionary[action]
|
21
|
+
|
22
|
+
context "when sending #{ action } Requests" do
|
23
|
+
it 'sets the token header' do
|
24
|
+
stub = stub_http_request(method, address)
|
25
|
+
response = subject.process(create_request(action, address))
|
26
|
+
expect(stub).to \
|
27
|
+
have_requested(method, address).with(headers: headers)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'returns Responses' do
|
31
|
+
stub_http_request(method, address).
|
32
|
+
to_return(code: '200', body: 'Hej!')
|
33
|
+
response = subject.process(create_request(action, address))
|
34
|
+
expect([ response.code, response.body ]).to eq([ 200, 'Hej!' ])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Helper do
|
4
|
+
extend ResourceHelper
|
5
|
+
|
6
|
+
let(:subject_module) { Apitizer::Helper }
|
7
|
+
|
8
|
+
describe '.member_action?' do
|
9
|
+
restful_member_actions.each do |action|
|
10
|
+
it "returns true for the #{ action } member action" do
|
11
|
+
expect(subject_module.member_action?(action)).to be_true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
restful_collection_actions.each do |action|
|
16
|
+
it "returns false for the #{ action } collection action" do
|
17
|
+
expect(subject_module.member_action?(action)).to be_false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'raises exceptions when encounters unknown actions' do
|
22
|
+
expect { subject_module.member_action?(:rock) }.to \
|
23
|
+
raise_error(subject_module::Error, /Unknown action/i)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '.deep_merge' do
|
28
|
+
it 'merges two hashes taking into account nested hashes' do
|
29
|
+
one = { a: 1, b: { c: 2, d: 3 } }
|
30
|
+
two = { a: 4, b: { c: 5, e: 6 } }
|
31
|
+
expect(subject_module.deep_merge(one, two)).to \
|
32
|
+
eq(a: 4, b: { c: 5, d: 3, e: 6 })
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '.build_query' do
|
37
|
+
it 'handels ordinary parameters' do
|
38
|
+
queries = [
|
39
|
+
'title=Meaning+of+Life&author=Random+Number+Generator',
|
40
|
+
'author=Random+Number+Generator&title=Meaning+of+Life'
|
41
|
+
]
|
42
|
+
query = subject_module.build_query(
|
43
|
+
title: 'Meaning of Life', author: 'Random Number Generator')
|
44
|
+
expect(queries).to include(query)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'handles parameters whose values are ordinary lists' do
|
48
|
+
query = subject_module.build_query(keywords: [ 'hitchhiker', 'galaxy' ])
|
49
|
+
expect(query).to eq('keywords[]=hitchhiker&keywords[]=galaxy')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'handles parameters whose values are object lists' do
|
53
|
+
queries = [
|
54
|
+
'genres[0][name]=Comedy&genres[1][name]=Fiction',
|
55
|
+
'genres[1][name]=Fiction&genres[0][name]=Comedy'
|
56
|
+
]
|
57
|
+
query = subject_module.build_query(
|
58
|
+
genres: { 0 => { name: 'Comedy' }, 1 => { name: 'Fiction' } })
|
59
|
+
expect(queries).to include(query)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'converts integers to decimal strings' do
|
63
|
+
query = subject_module.build_query(page: 42)
|
64
|
+
expect(query).to eq('page=42')
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'converts integers in object lists to decimal strings' do
|
68
|
+
queries = [
|
69
|
+
'primes[0][value]=2&primes[1][value]=3',
|
70
|
+
'primes[1][value]=3&primes[0][value]=2'
|
71
|
+
]
|
72
|
+
query = subject_module.build_query(
|
73
|
+
primes: { 0 => { value: 2 }, 1 => { value: 3 } })
|
74
|
+
expect(queries).to include(query)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'converts the logical true to the string true' do
|
78
|
+
query = subject_module.build_query(published: true)
|
79
|
+
expect(query).to eq('published=true')
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'converts the logical false to the string false' do
|
83
|
+
query = subject_module.build_query(published: false)
|
84
|
+
expect(query).to eq('published=false')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Processing::Parser do
|
4
|
+
let(:parent_module) { Apitizer::Processing }
|
5
|
+
let(:subject_class) { parent_module::Parser }
|
6
|
+
|
7
|
+
it 'supports JSON' do
|
8
|
+
subject = subject_class.build(:json)
|
9
|
+
result = subject.process('{ "articles": [] }')
|
10
|
+
expect(result).to eq("articles" => [])
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'supports YAML' do
|
14
|
+
subject = subject_class.build(:yaml)
|
15
|
+
result = subject.process("---\narticles: []")
|
16
|
+
expect(result).to eq("articles" => [])
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'does not support XML' do
|
20
|
+
expect { subject_class.build(:xml) }.to \
|
21
|
+
raise_error(parent_module::Error, /Unknown format/i)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Result do
|
4
|
+
let(:path) { double('Path') }
|
5
|
+
let(:request) { double('Request', path: path) }
|
6
|
+
let(:response) { double('Response', code: 200) }
|
7
|
+
let(:content) { double('Content') }
|
8
|
+
|
9
|
+
subject do
|
10
|
+
Apitizer::Result.new(request: request, response: response, content: content)
|
11
|
+
end
|
12
|
+
|
13
|
+
it { should == content }
|
14
|
+
it { should be_a(content.class) }
|
15
|
+
it { should be_kind_of(content.class) }
|
16
|
+
it { should be_instance_of(content.class) }
|
17
|
+
its(:path) { should == path }
|
18
|
+
its(:code) { should == 200 }
|
19
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Routing::Mapper do
|
4
|
+
extend ResourceHelper
|
5
|
+
|
6
|
+
let(:subject_class) { Apitizer::Routing::Mapper }
|
7
|
+
|
8
|
+
def create_path
|
9
|
+
double(:<< => nil, :advance => nil, :permitted? => true)
|
10
|
+
end
|
11
|
+
|
12
|
+
def expect_steps(steps, path = create_path)
|
13
|
+
Array(steps).each do |step|
|
14
|
+
expect(path).to receive(:<<).once.ordered.with(step)
|
15
|
+
end
|
16
|
+
path
|
17
|
+
end
|
18
|
+
|
19
|
+
def expect_trace(mapper, steps, scope = [])
|
20
|
+
mapper.trace(:arbitrary, steps, expect_steps(scope + steps))
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#define' do
|
24
|
+
it 'declares the root address' do
|
25
|
+
subject.define do
|
26
|
+
address('https://service.com/api')
|
27
|
+
resources(:articles)
|
28
|
+
end
|
29
|
+
expect_trace(subject, [ :articles, 'xxx' ], [ 'https://service.com/api' ])
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'declares plain resources' do
|
33
|
+
subject.define { resources(:articles) }
|
34
|
+
expect_trace(subject, [ :articles ])
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'declares nested resources' do
|
38
|
+
subject.define { resources(:articles) { resources(:sections) } }
|
39
|
+
expect_trace(subject, [ :articles, 'xxx', :sections, 'yyy' ])
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'declares scoped resources' do
|
43
|
+
subject.define do
|
44
|
+
scope 'https://service.com/api' do
|
45
|
+
scope [ 'v1', :json ] do
|
46
|
+
resources(:articles) { resources(:sections) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
expect_trace(subject, [ :articles, 'xxx', :sections, 'yyy' ],
|
51
|
+
[ 'https://service.com/api', 'v1', :json ])
|
52
|
+
end
|
53
|
+
|
54
|
+
restful_member_actions.each do |action|
|
55
|
+
it "declares custom #{ action } operations on members" do
|
56
|
+
subject.define do
|
57
|
+
resources(:articles) { send(action, :shred, on: :member) }
|
58
|
+
end
|
59
|
+
expect_trace(subject, [ :articles, 'xxx', :shred ])
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'declares custom operations with variable names' do
|
63
|
+
subject.define do
|
64
|
+
resources(:articles) { send(action, ':paragraph', on: :member) }
|
65
|
+
end
|
66
|
+
expect_trace(subject, [ :articles, 'xxx', 'zzz' ])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'does not support reopening of resource declarations' do
|
71
|
+
subject.define do
|
72
|
+
resources(:articles)
|
73
|
+
resources(:articles) { resources(:sections) }
|
74
|
+
end
|
75
|
+
expect do
|
76
|
+
subject.trace(:arbitrary, [ :articles, 'xxx', :sections, 'yyy' ])
|
77
|
+
end.to raise_error(Apitizer::Routing::Error, /Not found/i)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Routing::Node do
|
4
|
+
extend ResourceHelper
|
5
|
+
include FactoryHelper
|
6
|
+
|
7
|
+
shared_examples 'an adequate pathfinder' do
|
8
|
+
let(:path) { double(:<< => nil, :advance => nil) }
|
9
|
+
|
10
|
+
it 'gradually builds up Paths' do
|
11
|
+
steps.each do |step|
|
12
|
+
expect(path).to receive(:<<).once.ordered.with(step)
|
13
|
+
end
|
14
|
+
root.trace(steps, path)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'gradually advances Paths' do
|
18
|
+
steps.select { |step| step.is_a?(Symbol) }.each do |name|
|
19
|
+
expect(path).to receive(:advance).once.ordered.
|
20
|
+
with { |n| n.match(name) }
|
21
|
+
end
|
22
|
+
root.trace(steps, path)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '::Base#trace' do
|
27
|
+
context 'when working with plain collections' do
|
28
|
+
let(:root) { create_tree(:articles) }
|
29
|
+
|
30
|
+
context 'when looking for collections' do
|
31
|
+
let(:steps) { [ :articles ] }
|
32
|
+
it_behaves_like 'an adequate pathfinder'
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when looking for members' do
|
36
|
+
let(:steps) { [ :articles, 'xxx' ] }
|
37
|
+
it_behaves_like 'an adequate pathfinder'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when working with nested collections' do
|
42
|
+
let(:root) { create_tree(:articles, :sections) }
|
43
|
+
|
44
|
+
context 'when looking for collections' do
|
45
|
+
let(:steps) { [ :articles, 'xxx', :sections ] }
|
46
|
+
it_behaves_like 'an adequate pathfinder'
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when looking for members' do
|
50
|
+
let(:steps) { [ :articles, 'xxx', :sections, 'yyy' ] }
|
51
|
+
it_behaves_like 'an adequate pathfinder'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
restful_actions.each do |action|
|
56
|
+
context "when working with custom #{ action } actions" do
|
57
|
+
let(:root) { create_tree(:articles, shred: action) }
|
58
|
+
let(:steps) { [ :articles, 'xxx', :shred ] }
|
59
|
+
it_behaves_like 'an adequate pathfinder'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Routing::Path do
|
4
|
+
extend ResourceHelper
|
5
|
+
include FactoryHelper
|
6
|
+
|
7
|
+
describe '#<<' do
|
8
|
+
it 'builds up addresses' do
|
9
|
+
[ :articles, 'xxx', :sections, 'yyy' ].each { |step| subject << step }
|
10
|
+
expect(subject.address).to eq('articles/xxx/sections/yyy')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#advance' do
|
15
|
+
it 'keeps track of destinations' do
|
16
|
+
nodes = [ double('articles'), double('sections') ]
|
17
|
+
nodes.each { |node| subject.advance(node) }
|
18
|
+
expect(subject.node).to be(nodes.last)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
an_adequate_guard = Proc.new do |only_actions = restful_actions|
|
23
|
+
(restful_collection_actions & only_actions).each do |action|
|
24
|
+
it "is true for #{ action } collection action" do
|
25
|
+
path = root.trace(steps)
|
26
|
+
expect(path.permitted?(action)).to be_true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
(restful_member_actions & only_actions).each do |action|
|
31
|
+
it "is true for #{ action } member actions" do
|
32
|
+
path = root.trace([ *steps, 'xxx' ])
|
33
|
+
expect(path.permitted?(action)).to be_true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
(restful_collection_actions - only_actions).each do |action|
|
38
|
+
it "is false for #{ action } collection action" do
|
39
|
+
path = root.trace(steps)
|
40
|
+
expect(path.permitted?(action)).to be_false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
(restful_member_actions - only_actions).each do |action|
|
45
|
+
it "is false for #{ action } member actions" do
|
46
|
+
path = root.trace([ *steps, 'xxx' ])
|
47
|
+
expect(path.permitted?(action)).to be_false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
restful_member_actions.each do |action|
|
52
|
+
it "is false for #{ action } actions to collections" do
|
53
|
+
path = root.trace(steps)
|
54
|
+
expect(path.permitted?(action)).to be_false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
restful_collection_actions.each do |action|
|
59
|
+
it "is false for #{ action } actions to members" do
|
60
|
+
path = root.trace([ *steps, 'xxx' ])
|
61
|
+
expect(path.permitted?(action)).to be_false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#permitted?' do
|
67
|
+
context 'when working with plain collections' do
|
68
|
+
let(:root) { create_tree(:articles) }
|
69
|
+
let(:steps) { [ :articles ] }
|
70
|
+
|
71
|
+
instance_exec(&an_adequate_guard)
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when working with nested collections' do
|
75
|
+
let(:root) { create_tree(:articles, :sections) }
|
76
|
+
let(:steps) { [ :articles, 'yyy', :sections ] }
|
77
|
+
|
78
|
+
instance_exec(&an_adequate_guard)
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when working with collections restricted to index and show' do
|
82
|
+
let(:root) { create_tree([ :articles, [ :index, :show ] ]) }
|
83
|
+
let(:steps) { [ :articles ] }
|
84
|
+
|
85
|
+
instance_exec([ :index, :show ], &an_adequate_guard)
|
86
|
+
end
|
87
|
+
|
88
|
+
restful_member_actions.each do |action|
|
89
|
+
it "is true for custom #{ action } operations on members" do
|
90
|
+
root = create_tree(:articles, shred: action)
|
91
|
+
path = root.trace([ :articles, 'xxx', :shred ])
|
92
|
+
expect(path.permitted?(action)).to be_true
|
93
|
+
end
|
94
|
+
|
95
|
+
it "is true for custom #{ action } operations with variable names" do
|
96
|
+
root = create_tree(:articles, ':paragraph' => action)
|
97
|
+
path = root.trace([ :articles, 'xxx', 'zzz' ])
|
98
|
+
expect(path.permitted?(action)).to be_true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'support/resource_helper'
|
2
|
+
require 'support/factory_helper'
|
3
|
+
require 'webmock/rspec'
|
4
|
+
require 'apitizer'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
8
|
+
config.run_all_when_everything_filtered = true
|
9
|
+
config.filter_run :focus
|
10
|
+
config.order = 'random'
|
11
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module FactoryHelper
|
2
|
+
def create_tree(*names)
|
3
|
+
operations = names.last.is_a?(Hash) ? names.pop : {}
|
4
|
+
root = Apitizer::Routing::Node::Root.new
|
5
|
+
leaf = names.inject(root) do |parent, object|
|
6
|
+
if object.is_a?(Array)
|
7
|
+
name, only = *object
|
8
|
+
node = Apitizer::Routing::Node::Collection.new(name, only: only)
|
9
|
+
else
|
10
|
+
name = object
|
11
|
+
node = Apitizer::Routing::Node::Collection.new(name)
|
12
|
+
end
|
13
|
+
parent.append(node)
|
14
|
+
node
|
15
|
+
end
|
16
|
+
operations.each do |name, action|
|
17
|
+
operation = Apitizer::Routing::Node::Operation.new(
|
18
|
+
name, action: action, on: :member)
|
19
|
+
leaf.append(operation)
|
20
|
+
end
|
21
|
+
root
|
22
|
+
end
|
23
|
+
end
|