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,14 @@
|
|
1
|
+
require_relative 'parser/json'
|
2
|
+
require_relative 'parser/yaml'
|
3
|
+
|
4
|
+
module Apitizer
|
5
|
+
module Processing
|
6
|
+
module Parser
|
7
|
+
def self.build(format)
|
8
|
+
self.const_get(format.to_s.upcase).new
|
9
|
+
rescue NameError
|
10
|
+
raise Error, 'Unknown format'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Apitizer
|
2
|
+
class Result < SimpleDelegator
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def_delegator :@request, :path
|
6
|
+
def_delegator :@response, :code
|
7
|
+
def_delegators :__getobj__, :is_a?, :kind_of?, :instance_of?
|
8
|
+
|
9
|
+
def initialize(request:, response:, content:)
|
10
|
+
super(content)
|
11
|
+
@request = request
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Apitizer
|
2
|
+
module Routing
|
3
|
+
class Mapper
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegator :@root, :define_address
|
7
|
+
|
8
|
+
def initialize(&block)
|
9
|
+
@root = Node::Root.new
|
10
|
+
define(&block) if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
def trace(action, *arguments)
|
14
|
+
path = @root.trace(*arguments)
|
15
|
+
raise Error, 'Not permitted' unless path.permitted?(action)
|
16
|
+
path
|
17
|
+
end
|
18
|
+
|
19
|
+
def define(&block)
|
20
|
+
proxy = Proxy.new(self)
|
21
|
+
proxy.instance_eval(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def define_scope(path, parent: @root, &block)
|
25
|
+
child = Node::Scope.new(path)
|
26
|
+
parent.append(child)
|
27
|
+
proxy = Proxy.new(self, parent: child)
|
28
|
+
proxy.instance_eval(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def define_resources(name, parent: @root, **options, &block)
|
32
|
+
child = Node::Collection.new(name, **options)
|
33
|
+
parent.append(child)
|
34
|
+
return unless block_given?
|
35
|
+
proxy = Proxy.new(self, parent: child)
|
36
|
+
proxy.instance_eval(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
Apitizer.actions.each do |action|
|
40
|
+
define_method "define_#{ action }" do |name, parent:, **options|
|
41
|
+
child = Node::Operation.new(name, action: action, **options)
|
42
|
+
parent.append(child)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Apitizer
|
2
|
+
module Routing
|
3
|
+
module Node
|
4
|
+
class Base
|
5
|
+
def append(child)
|
6
|
+
children << child
|
7
|
+
end
|
8
|
+
|
9
|
+
def trace(steps, path = Path.new)
|
10
|
+
process(path, steps)
|
11
|
+
advance(path)
|
12
|
+
|
13
|
+
return path if steps.empty?
|
14
|
+
|
15
|
+
child = lookup(steps.first) or raise Error, 'Not found'
|
16
|
+
child.trace(steps, path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def match(name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(path, steps)
|
23
|
+
end
|
24
|
+
|
25
|
+
def permitted?(action, path)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def children
|
31
|
+
@children ||= []
|
32
|
+
end
|
33
|
+
|
34
|
+
def lookup(name)
|
35
|
+
children.find { |c| c.match(name) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def advance(path)
|
39
|
+
path.advance(self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Apitizer
|
2
|
+
module Routing
|
3
|
+
module Node
|
4
|
+
class Collection < Base
|
5
|
+
def initialize(name, only: nil)
|
6
|
+
@name = name
|
7
|
+
@actions = only && Array(only) || Apitizer.actions
|
8
|
+
unless (@actions - Apitizer.actions).empty?
|
9
|
+
raise Error, 'Not supported'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def match(name)
|
14
|
+
@name == name
|
15
|
+
end
|
16
|
+
|
17
|
+
def process(path, steps)
|
18
|
+
path << steps.shift # @name
|
19
|
+
return path if steps.empty?
|
20
|
+
path << steps.shift # id
|
21
|
+
end
|
22
|
+
|
23
|
+
def permitted?(action, path)
|
24
|
+
return false unless @actions.include?(action)
|
25
|
+
|
26
|
+
id_present = path.steps.last != @name
|
27
|
+
member_action = Helper.member_action?(action)
|
28
|
+
|
29
|
+
id_present == member_action
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Apitizer
|
2
|
+
module Routing
|
3
|
+
module Node
|
4
|
+
class Operation < Base
|
5
|
+
def initialize(name, action:, on:, **options)
|
6
|
+
# TODO: how about on == :collection?
|
7
|
+
unless Apitizer.actions.include?(action) && on == :member
|
8
|
+
raise Error, 'Not supported'
|
9
|
+
end
|
10
|
+
@name = name
|
11
|
+
@action = action
|
12
|
+
end
|
13
|
+
|
14
|
+
def match(name)
|
15
|
+
if @name.is_a?(String) && @name =~ /^:/
|
16
|
+
true
|
17
|
+
else
|
18
|
+
@name == name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(path, steps)
|
23
|
+
path << steps.shift # @name
|
24
|
+
end
|
25
|
+
|
26
|
+
def permitted?(action, path)
|
27
|
+
@action == action
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Apitizer
|
2
|
+
module Routing
|
3
|
+
module Node
|
4
|
+
class Scope < Base
|
5
|
+
def initialize(steps)
|
6
|
+
@steps = Array(steps)
|
7
|
+
end
|
8
|
+
|
9
|
+
def match(name)
|
10
|
+
not lookup(name).nil?
|
11
|
+
end
|
12
|
+
|
13
|
+
def process(path, steps)
|
14
|
+
@steps.each { |step| path << step }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Apitizer
|
2
|
+
module Routing
|
3
|
+
class Path
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_reader :steps, :node
|
7
|
+
def_delegators :steps, :<<
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@steps = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def address
|
14
|
+
@steps.map(&:to_s).join('/')
|
15
|
+
end
|
16
|
+
|
17
|
+
def advance(node)
|
18
|
+
@node = node
|
19
|
+
end
|
20
|
+
|
21
|
+
def permitted?(action)
|
22
|
+
@node && @node.permitted?(action, self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Apitizer
|
2
|
+
module Routing
|
3
|
+
class Proxy
|
4
|
+
def initialize(owner, **options)
|
5
|
+
@owner = owner
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(name, *arguments, **options, &block)
|
10
|
+
name = :"define_#{ name }"
|
11
|
+
return super unless @owner.respond_to?(name)
|
12
|
+
# NOTE: https://bugs.ruby-lang.org/issues/9776
|
13
|
+
@owner.send(name, *arguments, **options, **@options, &block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Base do
|
4
|
+
extend ResourceHelper
|
5
|
+
|
6
|
+
let(:subject_class) { Apitizer::Base }
|
7
|
+
let(:address) { 'https://service.com/api' }
|
8
|
+
|
9
|
+
def stub_request(method, address)
|
10
|
+
stub_http_request(method, "https://service.com/api/#{ address }").
|
11
|
+
to_return(code: '200', body: '{}')
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#new' do
|
15
|
+
it 'does not require any arguments' do
|
16
|
+
expect { subject_class.new }.not_to raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'draws a routing map when a block is given' do
|
20
|
+
scope_name = address
|
21
|
+
subject = subject_class.new { scope(scope_name) { resources(:articles) } }
|
22
|
+
stub_request(:get, 'articles')
|
23
|
+
expect { subject.process(:index, :articles) }.not_to raise_error
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'customizes the mapping between the REST actions and HTTP verbs' do
|
27
|
+
scope_name = address
|
28
|
+
subject = subject_class.new(dictionary: { update: :delete }) do
|
29
|
+
scope(scope_name) { resources(:articles) }
|
30
|
+
end
|
31
|
+
stub = stub_request(:delete, 'articles/xxx')
|
32
|
+
subject.process(:update, :articles, 'xxx')
|
33
|
+
expect(stub).to have_been_requested
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#process' do
|
38
|
+
subject do
|
39
|
+
scope_name = address
|
40
|
+
subject_class.new { scope(scope_name) { resources(:articles) } }
|
41
|
+
end
|
42
|
+
|
43
|
+
restful_collection_actions.each do |action|
|
44
|
+
method = rest_http_dictionary[action]
|
45
|
+
|
46
|
+
it "is capable of #{ action } actions" do
|
47
|
+
stub_request(method, 'articles')
|
48
|
+
expect { subject.process(action, :articles) }.not_to raise_error
|
49
|
+
end
|
50
|
+
|
51
|
+
it "is capable of #{ action } actions via alias" do
|
52
|
+
stub_request(method, 'articles')
|
53
|
+
expect { subject.send(action, :articles) }.not_to raise_error
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
restful_member_actions.each do |action|
|
58
|
+
method = rest_http_dictionary[action]
|
59
|
+
|
60
|
+
it "is capable of #{ action } actions" do
|
61
|
+
stub_request(method, 'articles/xxx')
|
62
|
+
expect { subject.process(action, :articles, 'xxx') }.not_to raise_error
|
63
|
+
end
|
64
|
+
|
65
|
+
it "is capable of #{ action } actions via alias" do
|
66
|
+
stub_request(method, 'articles/xxx')
|
67
|
+
expect { subject.send(action, :articles, 'xxx') }.not_to raise_error
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Apitizer::Connection::Adaptor do
|
4
|
+
let(:parent_module) { Apitizer::Connection }
|
5
|
+
let(:address) { 'https://service.com/api/v1/json/articles' }
|
6
|
+
|
7
|
+
[ 'Standard' ].each do |adaptor|
|
8
|
+
subject { parent_module::Adaptor.const_get(adaptor).new }
|
9
|
+
|
10
|
+
describe "#{ adaptor }.process" do
|
11
|
+
it 'returns the code, headers, and body of the response' do
|
12
|
+
stub_http_request(:get, address).to_return(
|
13
|
+
code: '200', body: 'Hej!', headers: { 'a' => 'b' } )
|
14
|
+
response = subject.process(:get, address)
|
15
|
+
expect(response).to eq([ '200', { 'a' => [ 'b' ] }, 'Hej!' ])
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'raises exceptions when encounters unknown methods' do
|
19
|
+
expect { subject.process(:smile, address) }.to \
|
20
|
+
raise_error(parent_module::Error, /Invalid method/i)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|