apitizer 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.
- 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
|