callapi 0.8

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.
@@ -0,0 +1,41 @@
1
+ require 'colorize'
2
+
3
+ module Callapi::Call::Request::Http::LogHelper
4
+ def with_logging
5
+ return yield if Callapi::Config.log_level == :none
6
+
7
+ t0 = Time.now
8
+ string = ''
9
+ string << uri.host
10
+ string << ":#{uri.port}" if uri.port
11
+
12
+ puts "Sending request to #{string}".center(80, '-').colorize(:white).on_blue
13
+ 'PATH: '.tap do |string|
14
+ string << "#{request_method.to_s.upcase} "
15
+ string << "#{uri.path}"
16
+ puts string.colorize(:magenta)
17
+ end
18
+ 'HEADERS: '.tap do |string|
19
+ string << "#{headers}"
20
+ puts string.colorize(:cyan)
21
+ end
22
+ 'PARAMS: '.tap do |string|
23
+ string << "#{params}"
24
+ puts string.colorize(:green)
25
+ end
26
+
27
+ response = yield
28
+
29
+ response.tap do |response|
30
+ "RESPONSE: [#{response.code}]\n".tap do |string|
31
+ string << (response.body.nil? ? '[EMPTY BODY]' : response.body)
32
+ puts string.colorize(:light_blue)
33
+ end
34
+
35
+ puts "request send (#{(Time.now - t0).round(3)} sec)".center(80, '-').colorize(:white).on_blue
36
+ end
37
+ rescue StandardError => e
38
+ puts "Exception occured, skipping logs".center(80, '-').colorize(:red).on_yellow
39
+ raise e
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ class Callapi::Call::Request::Mock < Callapi::Call::Request::Base
2
+ extend Memoist
3
+ #TODO: should not be hardcoded
4
+ MOCK_FORMAT = '.json'
5
+
6
+ def response
7
+ OpenStruct.new(body: body, code: code)
8
+ end
9
+
10
+ def code
11
+ '200'
12
+ end
13
+
14
+ def body
15
+ File.read(file_path) # add #with_logging
16
+ rescue Errno::ENOENT
17
+ raise Callapi::CouldNotFoundMockRequestFileError.new(file_path)
18
+ end
19
+
20
+ private
21
+
22
+ def file_path
23
+ File.join(Callapi::Config.mocks_directory, @context.request_method.to_s, file_name + MOCK_FORMAT)
24
+ end
25
+
26
+ def file_name
27
+ @context.request_path
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ class Callapi::Call::RequestMetadata < Struct.new(:context)
4
+ extend Memoist
5
+
6
+ HTTP_METHODS = %w(GET POST PUT PATCH DELETE)
7
+
8
+ def request_method
9
+ http_method = namespace_with_http_method
10
+ raise Callapi::UnknownHttpMethodError unless http_method
11
+ http_method.downcase.to_sym
12
+ end
13
+
14
+ def request_path
15
+ request_path = request_path_without_replaced_param_keys
16
+ param_keys_to_replace.each do |param_key|
17
+ param_value = context.params[param_key.to_sym] || raise(Callapi::MissingParamError.new(request_path, param_keys_to_replace, missing_keys))
18
+ request_path.sub!(param_key + '_param', param_value.to_s)
19
+ end
20
+ request_path
21
+ end
22
+ memoize :request_path
23
+
24
+ private
25
+
26
+ def param_keys_to_replace
27
+ request_path_without_replaced_param_keys.scan(/(\w+)_param/).map(&:first)
28
+ end
29
+
30
+ def request_path_without_replaced_param_keys
31
+ '/' + call_name.underscore
32
+ end
33
+ memoize :request_path_without_replaced_param_keys
34
+
35
+ def missing_keys
36
+ (param_keys_to_replace.map(&:to_sym) - context.params.keys).map { |key| ":#{key}" }
37
+ end
38
+
39
+ def namespaces_after_http_method
40
+ namespaces[namespaces.index(namespace_with_http_method) + 1 .. namespaces.size]
41
+ end
42
+ memoize :namespaces_after_http_method
43
+
44
+ def namespace_with_http_method
45
+ namespaces.detect{ |namespace| HTTP_METHODS.include?(namespace.upcase) } || raise(Callapi::UnknownHttpMethodError)
46
+ end
47
+ memoize :namespace_with_http_method
48
+
49
+ def call_name
50
+ namespaces_after_http_method.join('::')
51
+ end
52
+
53
+ def namespaces
54
+ context.class.to_s.split('::')
55
+ end
56
+ memoize :namespaces
57
+ end
@@ -0,0 +1,54 @@
1
+ # Change it to Callapi::Call::Parser::Base
2
+ class Callapi::Call::Parser
3
+ require_relative 'response/plain'
4
+ require_relative 'response/json'
5
+ require_relative 'response/json/as_object'
6
+
7
+ extend Memoist
8
+ extend Forwardable
9
+
10
+ def_delegators :@response, :body, :code
11
+
12
+ def initialize(response)
13
+ @response = response
14
+ end
15
+
16
+ def data
17
+ raise_error unless ok?
18
+ return nil if no_content?
19
+
20
+ parse
21
+ end
22
+ memoize :data
23
+
24
+ def status
25
+ code.to_i
26
+ end
27
+ memoize :status
28
+
29
+ private
30
+
31
+ def parse
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def ok?
36
+ status < 300
37
+ end
38
+ memoize :ok?
39
+
40
+ def no_content?
41
+ return true if body.nil?
42
+ body.strip.empty?
43
+ end
44
+ memoize :no_content?
45
+
46
+ def raise_error
47
+ error_class = Callapi::Errors.error_by_status(status)
48
+ raise error_class.new(status, error_message)
49
+ end
50
+
51
+ def error_message
52
+ "response body: \"#{body}\""
53
+ end
54
+ end
@@ -0,0 +1,18 @@
1
+ require 'multi_json'
2
+
3
+ class Callapi::Call::Parser::Json < Callapi::Call::Parser
4
+ def parse
5
+ to_hash
6
+ end
7
+
8
+ private
9
+
10
+ def to_hash
11
+ @to_hash ||= MultiJson.load(body)
12
+ end
13
+ #TODO: remove Memoist
14
+
15
+ def error_message
16
+ to_hash['error_message'] rescue super
17
+ end
18
+ end
@@ -0,0 +1,49 @@
1
+ require_relative '../../../../ext/deep_struct'
2
+
3
+ class Callapi::Call::Parser::Json::AsObject < Callapi::Call::Parser::Json
4
+ def parse
5
+ object.tap do |struct|
6
+ append_data_excluded_from_parsing(struct)
7
+ end
8
+ end
9
+
10
+ def self.keys_excluded_from_parsing
11
+ @keys_excluded_from_parsing ||= []
12
+ end
13
+
14
+ def self.keys_excluded_from_parsing=(keys_excluded_from_parsing)
15
+ @keys_excluded_from_parsing = keys_excluded_from_parsing
16
+ end
17
+
18
+ private
19
+
20
+ def object
21
+ if data_to_parse.is_a?(Array)
22
+ data_to_parse.map { |item| DeepStruct.new(item) }
23
+ else
24
+ DeepStruct.new(data_to_parse)
25
+ end
26
+ end
27
+
28
+ def data_to_parse
29
+ keys_excluded_from_parsing = self.class.keys_excluded_from_parsing
30
+ to_hash.dup.delete_if { |key, value| keys_excluded_from_parsing.include? key }
31
+ end
32
+
33
+ def data_excluded_from_parsing
34
+ @data_excluded_from_parsing ||= {}.tap do |hash|
35
+ self.class.keys_excluded_from_parsing.each do |key|
36
+ hash[key] = to_hash[key]
37
+ end
38
+ end
39
+ end
40
+
41
+ def append_data_excluded_from_parsing(struct)
42
+ return if struct.is_a?(Array)
43
+ struct.tap do |struct|
44
+ self.class.keys_excluded_from_parsing.each do |key|
45
+ struct.send("#{key}=", data_excluded_from_parsing[key])
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ class Callapi::Call::Parser::Plain < Callapi::Call::Parser
2
+ def parse
3
+ body
4
+ end
5
+ end
@@ -0,0 +1,56 @@
1
+ class Callapi::Config
2
+ DEFAULT_REQUEST_STRATEGY = 'Callapi::Call::Request::Api'
3
+ DEFAULT_RESPONSE_PARSER = 'Callapi::Call::Parser::Json'
4
+ DEFAULT_MOCKS_DIRECTORY = 'mocked_calls'
5
+ DEFAULT_PATH_PREFIX = ''
6
+
7
+ class << self
8
+ attr_reader :mocks_directory
9
+ attr_accessor :api_host
10
+ attr_writer :api_path_prefix, :default_response_parser, :default_request_strategy
11
+
12
+ def configure
13
+ yield self if block_given?
14
+ end
15
+
16
+ def default_request_strategy
17
+ @default_request_strategy ||= DEFAULT_REQUEST_STRATEGY.constantize
18
+ end
19
+
20
+ def api_path_prefix
21
+ @api_path_prefix ||= DEFAULT_PATH_PREFIX
22
+ end
23
+
24
+ def default_response_parser
25
+ @default_response_parser ||= DEFAULT_RESPONSE_PARSER.constantize
26
+ end
27
+
28
+ def mocks_directory=(mocks_directory)
29
+ [].tap do |paths|
30
+ paths << Rails.root if defined?(Rails)
31
+ paths << mocks_directory
32
+ paths << '/'
33
+ @mocks_directory = File.join(paths)
34
+ end
35
+ end
36
+
37
+ def mocks_directory
38
+ return @mocks_directory if @mocks_directory
39
+
40
+ [].tap do |paths|
41
+ paths << Rails.root if defined?(Rails)
42
+ paths << DEFAULT_MOCKS_DIRECTORY
43
+ paths << '/'
44
+ @mocks_directory = File.join(paths)
45
+ end
46
+ end
47
+
48
+ def log_level=(log_level)
49
+ @log_level = log_level
50
+ end
51
+
52
+ def log_level
53
+ @log_level ||= :debug
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,58 @@
1
+ class Callapi::Errors < StandardError
2
+ STATUS_TO_ERROR_CLASS = {
3
+ 401 => 'NotAuthorizedError',
4
+ 404 => 'NotFoundError'
5
+ }
6
+ def self.error_by_status(status)
7
+ error_class_name = STATUS_TO_ERROR_CLASS[status]
8
+ unless error_class_name
9
+ error_class_name = case status
10
+ when 500..599 then 'ServerError'
11
+ when 400..499 then 'ClientError'
12
+ when 300..399 then 'RedirectionError'
13
+ else
14
+ 'ServerError'
15
+ end
16
+ end
17
+ "Callapi::#{error_class_name}".constantize
18
+ end
19
+ end
20
+
21
+ class Callapi::ApiError < StandardError
22
+ def initialize(status, message)
23
+ super "#{status}: #{message}"
24
+ end
25
+ end
26
+
27
+ class Callapi::ServerError < Callapi::ApiError; end
28
+ class Callapi::ClientError < Callapi::ApiError; end
29
+ class Callapi::RedirectionError < Callapi::ApiError; end
30
+ class Callapi::NotAuthorizedError < Callapi::ApiError; end
31
+ class Callapi::NotFoundError < Callapi::ApiError; end
32
+
33
+ class Callapi::UnknownHttpMethodError < StandardError
34
+ def initialize
35
+ super 'Could not retrieve HTTP method from Call class name'
36
+ end
37
+ end
38
+
39
+ class Callapi::ApiHostNotSetError < StandardError
40
+ def initialize
41
+ super 'Set API host with Callapi::Config.api_host = "http://yourapi.host.com"'
42
+ end
43
+ end
44
+
45
+ class Callapi::CouldNotFoundMockRequestFileError < StandardError
46
+ def initialize(file_path)
47
+ super "Expected \"#{file_path}\""
48
+ end
49
+ end
50
+
51
+ class Callapi::MissingParamError < StandardError
52
+ def initialize(request_path, param_keys_to_replace, missing_keys)
53
+ param_keys_to_replace.each do |param_key|
54
+ request_path.sub!(param_key + '_param', ':' + param_key)
55
+ end
56
+ super "could not found: #{missing_keys.join(', ')} for \"#{request_path}\""
57
+ end
58
+ end
@@ -0,0 +1,131 @@
1
+ require 'active_support/core_ext/string'
2
+
3
+ class Callapi::Routes
4
+ require_relative 'routes/metadata'
5
+
6
+ class << self
7
+ def draw(&block)
8
+ build_http_method_namespaces
9
+
10
+ instance_eval &block
11
+
12
+ create_classes
13
+ end
14
+
15
+ def get(*args)
16
+ save_route(Callapi::Get, *args)
17
+ end
18
+
19
+ def post(*args)
20
+ save_route(Callapi::Post, *args)
21
+ end
22
+
23
+ def put(*args)
24
+ save_route(Callapi::Put, *args)
25
+ end
26
+
27
+ def delete(*args)
28
+ save_route(Callapi::Delete, *args)
29
+ end
30
+
31
+ def patch(*args)
32
+ save_route(Callapi::Patch, *args)
33
+ end
34
+
35
+ def namespace(*args)
36
+ add_namespace(args.shift)
37
+ yield
38
+ remove_namespace
39
+ end
40
+
41
+ private
42
+
43
+ def save_route(http_method_namespace, *args)
44
+ Callapi::Routes::Metadata.new(http_method_namespace, *args)
45
+ end
46
+
47
+ def create_classes
48
+ classes_metadata.each do |class_metadata|
49
+ classes = class_metadata.class_name.split('::')
50
+ classes = classes[2..classes.size]
51
+
52
+ classes.inject(class_metadata.http_method_namespace) do |namespace, class_name|
53
+ if namespace.constants.include?(class_name.to_sym)
54
+ namespace.const_get(class_name)
55
+ else
56
+ full_class_name = "#{namespace}::#{class_name}"
57
+ if call_classes_names.include?(full_class_name)
58
+ namespace.const_set(class_name, Class.new(Callapi::Call::Base)).tap do |klass|
59
+ set_call_class_options(klass, class_metadata.class_options) if class_metadata.class_options
60
+ create_helper_method(klass, class_metadata)
61
+ end
62
+ else
63
+ namespace.const_set(class_name, Class.new)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def create_helper_method(klass, class_metadata)
71
+ http_method = class_metadata.http_method_namespace.to_s.split('::').last
72
+ call_name_with_namespaces = class_metadata.call_name_with_namespaces.map do |class_name|
73
+ class_name.scan(/(::)?((\w)+)Param/).map { |matched_groups| matched_groups[1] }.compact.each do |pattern|
74
+ class_name.sub!(pattern, "By#{pattern}")
75
+ class_name.sub!('Param', '')
76
+ end
77
+ class_name
78
+ end
79
+ method_name = [http_method, call_name_with_namespaces, 'call'].join('_')
80
+ method_name = method_name.underscore.gsub('/', '_')
81
+ Object.send(:define_method, method_name) do |*args|
82
+ klass.new(*args)
83
+ end
84
+ end
85
+
86
+ def set_call_class_options(klass, options)
87
+ klass.strategy = options[:strategy] if options[:strategy]
88
+ klass.response_parser = options[:parser] if options[:parser]
89
+ end
90
+
91
+ def namespaces
92
+ @namespaces ||= []
93
+ end
94
+
95
+ def add_namespace(namespace)
96
+ namespaces << namespace.to_s
97
+ end
98
+
99
+ def remove_namespace
100
+ namespaces.pop
101
+ end
102
+
103
+ def build_http_method_namespaces
104
+ @build_http_method_namespaces ||= http_methods.each do |http_method|
105
+ Callapi.const_set(http_method.to_s.camelize, Module.new)
106
+ end
107
+ end
108
+
109
+ def http_methods
110
+ Callapi::Call::Request::Http::HTTP_METHOD_TO_REQUEST_CLASS.keys
111
+ end
112
+
113
+ def save_class(class_metadata)
114
+ classes_metadata << class_metadata unless classes_metadata.include?(class_metadata)
115
+ @call_classes = nil
116
+ @call_classes_names = nil
117
+ end
118
+
119
+ def classes_metadata
120
+ @classes_metadata ||= []
121
+ end
122
+
123
+ def call_classes_metadata
124
+ @call_classes ||= classes_metadata.select(&:call_class)
125
+ end
126
+
127
+ def call_classes_names
128
+ @call_classes_names ||= call_classes_metadata.map(&:class_name).uniq
129
+ end
130
+ end
131
+ end