apitizer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +1 -0
  5. data/Gemfile +3 -0
  6. data/Guardfile +7 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +91 -0
  9. data/Rakefile +7 -0
  10. data/apitizer.gemspec +32 -0
  11. data/lib/apitizer.rb +14 -0
  12. data/lib/apitizer/base.rb +61 -0
  13. data/lib/apitizer/connection.rb +10 -0
  14. data/lib/apitizer/connection/adaptor.rb +13 -0
  15. data/lib/apitizer/connection/adaptor/standard.rb +34 -0
  16. data/lib/apitizer/connection/dispatcher.rb +24 -0
  17. data/lib/apitizer/connection/request.rb +16 -0
  18. data/lib/apitizer/connection/response.rb +12 -0
  19. data/lib/apitizer/core.rb +24 -0
  20. data/lib/apitizer/helper.rb +46 -0
  21. data/lib/apitizer/processing.rb +8 -0
  22. data/lib/apitizer/processing/parser.rb +14 -0
  23. data/lib/apitizer/processing/parser/json.rb +15 -0
  24. data/lib/apitizer/processing/parser/yaml.rb +15 -0
  25. data/lib/apitizer/processing/translator.rb +13 -0
  26. data/lib/apitizer/result.rb +15 -0
  27. data/lib/apitizer/routing.rb +10 -0
  28. data/lib/apitizer/routing/mapper.rb +47 -0
  29. data/lib/apitizer/routing/node.rb +5 -0
  30. data/lib/apitizer/routing/node/base.rb +44 -0
  31. data/lib/apitizer/routing/node/collection.rb +34 -0
  32. data/lib/apitizer/routing/node/operation.rb +32 -0
  33. data/lib/apitizer/routing/node/root.rb +15 -0
  34. data/lib/apitizer/routing/node/scope.rb +19 -0
  35. data/lib/apitizer/routing/path.rb +26 -0
  36. data/lib/apitizer/routing/proxy.rb +17 -0
  37. data/lib/apitizer/version.rb +3 -0
  38. data/spec/apitizer/base_spec.rb +71 -0
  39. data/spec/apitizer/connection/adaptor_spec.rb +24 -0
  40. data/spec/apitizer/connection/dispatcher_spec.rb +39 -0
  41. data/spec/apitizer/helper_spec.rb +87 -0
  42. data/spec/apitizer/processing/parser_spec.rb +23 -0
  43. data/spec/apitizer/result_spec.rb +19 -0
  44. data/spec/apitizer/routing/mapper_spec.rb +80 -0
  45. data/spec/apitizer/routing/node_spec.rb +63 -0
  46. data/spec/apitizer/routing/path_spec.rb +102 -0
  47. data/spec/spec_helper.rb +11 -0
  48. data/spec/support/factory_helper.rb +23 -0
  49. data/spec/support/resource_helper.rb +18 -0
  50. 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
@@ -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