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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +207 -0
- data/Rakefile +2 -0
- data/callapi.gemspec +31 -0
- data/lib/callapi.rb +12 -0
- data/lib/callapi/call.rb +3 -0
- data/lib/callapi/call/base.rb +66 -0
- data/lib/callapi/call/request.rb +14 -0
- data/lib/callapi/call/request/api.rb +9 -0
- data/lib/callapi/call/request/base.rb +13 -0
- data/lib/callapi/call/request/http.rb +60 -0
- data/lib/callapi/call/request/http/log_helper.rb +41 -0
- data/lib/callapi/call/request/mock.rb +29 -0
- data/lib/callapi/call/request_metadata.rb +57 -0
- data/lib/callapi/call/response.rb +54 -0
- data/lib/callapi/call/response/json.rb +18 -0
- data/lib/callapi/call/response/json/as_object.rb +49 -0
- data/lib/callapi/call/response/plain.rb +5 -0
- data/lib/callapi/config.rb +56 -0
- data/lib/callapi/errors.rb +58 -0
- data/lib/callapi/routes.rb +131 -0
- data/lib/callapi/routes/metadata.rb +72 -0
- data/lib/callapi/version.rb +3 -0
- data/lib/ext/deep_struct.rb +25 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/unit/call/base_spec.rb +38 -0
- data/spec/unit/call/request/api_spec.rb +41 -0
- data/spec/unit/call/request/mock_spec.rb +38 -0
- data/spec/unit/call/request_metadata_spec.rb +58 -0
- data/spec/unit/call/response/json/as_object_spec.rb +48 -0
- data/spec/unit/call/response/json_spec.rb +21 -0
- data/spec/unit/call/response/plain_spec.rb +23 -0
- data/spec/unit/call/response_spec.rb +61 -0
- data/spec/unit/routes_spec.rb +123 -0
- metadata +216 -0
@@ -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,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
|