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