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,8 @@
1
+ require_relative 'processing/parser'
2
+ require_relative 'processing/translator'
3
+
4
+ module Apitizer
5
+ module Processing
6
+ Error = Class.new(Apitizer::Error)
7
+ end
8
+ end
@@ -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
+ require 'json'
2
+
3
+ module Apitizer
4
+ module Processing
5
+ module Parser
6
+ class JSON
7
+ def process(data)
8
+ ::JSON.parse(data)
9
+ rescue
10
+ raise Error, 'Unable to parse'
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require 'yaml'
2
+
3
+ module Apitizer
4
+ module Processing
5
+ module Parser
6
+ class YAML
7
+ def process(data)
8
+ ::YAML.load(data)
9
+ rescue
10
+ raise Error, 'Unable to parse'
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module Apitizer
2
+ module Processing
3
+ class Translator
4
+ def initialize(format:)
5
+ @parser = Parser.build(format)
6
+ end
7
+
8
+ def process(response)
9
+ @parser.process(response.body)
10
+ end
11
+ end
12
+ end
13
+ 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,10 @@
1
+ require_relative 'routing/path'
2
+ require_relative 'routing/node'
3
+ require_relative 'routing/proxy'
4
+ require_relative 'routing/mapper'
5
+
6
+ module Apitizer
7
+ module Routing
8
+ Error = Class.new(Apitizer::Error)
9
+ end
10
+ 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,5 @@
1
+ require_relative 'node/base'
2
+ require_relative 'node/root'
3
+ require_relative 'node/scope'
4
+ require_relative 'node/collection'
5
+ require_relative 'node/operation'
@@ -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,15 @@
1
+ module Apitizer
2
+ module Routing
3
+ module Node
4
+ class Root < Base
5
+ def define_address(address, *_)
6
+ @address = address
7
+ end
8
+
9
+ def process(path, steps)
10
+ path << @address unless @address.nil?
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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,3 @@
1
+ module Apitizer
2
+ VERSION = '0.0.1'
3
+ 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