restool 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 00b59168b2f8bf6dda90e1764dfb4e86b31fc35b9467a6dbe128fa456747cfe3
4
+ data.tar.gz: '09267a1659146bfafdc1433ea3164f071f8f9af8325c405dbb145918ec630aa5'
5
+ SHA512:
6
+ metadata.gz: bc937386969581255334f5c648b1241c2eb59867f54cb4eaa2c968c6a3f1a1c29d781ef8ad621a554a4c21a2e35f576d20c7de0ba873c8383db784596f318943
7
+ data.tar.gz: 6dae0d57818581ce78f335b814adc9921f81ffa5279d7848b0612dc8b657ba15226740c6e254df86e99f53c60e99c75c3fd99546a477d86dc600f860d408ea2b
data/lib/restool.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative 'restool/settings/loader'
2
+ require_relative 'restool/service/restool_service'
3
+
4
+
5
+ module Restool
6
+
7
+ def self.create(service_name, &response_handler)
8
+ service_config = Restool::Settings::Loader.load(service_name)
9
+
10
+ Restool::Service::RestoolService.new(service_config, response_handler)
11
+ end
12
+
13
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'uri_utils'
2
+
3
+ module Restool
4
+ module Service
5
+ module OperationDefiner
6
+
7
+ def define_operations(service_config, method_make_request, method_make_request_with_uri_params)
8
+ service_config.operations.each do |operation|
9
+ if operation.uri_params != []
10
+ define_request_method_with_uri_params(operation, method_make_request_with_uri_params)
11
+ else
12
+ define_request_method(operation, method_make_request)
13
+ end
14
+ end
15
+ end
16
+
17
+ def define_request_method_with_uri_params(operation, method_make_request_with_uri_params)
18
+ define_singleton_method(operation.name) do |uri_params_values, *params|
19
+ method_make_request_with_uri_params.call(operation, uri_params_values, params[0], params[1])
20
+ end
21
+ end
22
+
23
+ def define_request_method(operation, method_make_request)
24
+ define_singleton_method(operation.name) do |*params|
25
+ method_make_request.call(operation, params[0], params[1])
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ require 'persistent_http'
2
+
3
+ require_relative 'request_utils'
4
+
5
+ module Restool
6
+ module Service
7
+ class RemoteClient
8
+
9
+ def initialize(host, verify_ssl, persistent_connection, timeout)
10
+ @connection = if persistent_connection
11
+ PersistentHTTP.new(
12
+ pool_size: persistent_connection.pool_size,
13
+ pool_timeout: timeout,
14
+ warn_timeout: persistent_connection.warn_timeout,
15
+ force_retry: persistent_connection.force_retry,
16
+ url: host,
17
+ read_timeout: timeout,
18
+ open_timeout: timeout,
19
+ verify_mode: verify_ssl?(verify_ssl)
20
+ )
21
+ else
22
+ uri = URI.parse(host)
23
+ http = Net::HTTP.new(uri.host, uri.port)
24
+ http.use_ssl = ssl_implied?(uri)
25
+ http.verify_mode = verify_ssl?(verify_ssl)
26
+ http.read_timeout = timeout
27
+ http.open_timeout = timeout
28
+ # http.set_debug_output($stdout)
29
+ http
30
+ end
31
+ end
32
+
33
+ def make_request(path, method, request_params, headers, basic_auth)
34
+ request = RequestUtils.build_request(method, path, request_params, headers, basic_auth)
35
+
36
+ @connection.request(request)
37
+ end
38
+
39
+ private
40
+
41
+ def ssl_implied?(uri)
42
+ uri.port == 443 || uri.scheme == 'https'
43
+ end
44
+
45
+ def verify_ssl?(verify_ssl_setting)
46
+ verify_ssl_setting ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,24 @@
1
+ require_relative '../traversal/converter'
2
+
3
+ module Restool
4
+ module Service
5
+ module RemoteConnector
6
+ # The RemoteConnector module makes the requests using the RemoteClient,
7
+ # calls the response_handler with the response, and finally executes
8
+ # the object traversal
9
+
10
+ def self.execute(remote_client, operation, path, params, headers,
11
+ response_handler, representations, basic_auth)
12
+ remote_response = remote_client.make_request(path, operation.method, params, headers,
13
+ basic_auth)
14
+
15
+ response = response_handler.call(remote_response.body, remote_response.code)
16
+
17
+ return response if operation.response.nil?
18
+
19
+ Restool::Traversal::Converter.convert(response, operation.response, representations)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ module Restool
2
+ module Service
3
+ module RequestUtils
4
+
5
+ def self.build_request(method, path, params, headers, basic_auth)
6
+ if ['post', 'put', 'patch'].include?(method)
7
+ request = build_base_request(method, path)
8
+
9
+ if params && params.is_a?(Hash)
10
+ request.set_form_data(params)
11
+ else
12
+ request.body = params
13
+ end
14
+
15
+ else
16
+ uri = URI(path)
17
+ uri.query = URI.encode_www_form(params) if params
18
+
19
+ request = build_base_request(method, uri.to_s)
20
+ end
21
+
22
+ request.basic_auth(basic_auth.user, basic_auth.password) if basic_auth
23
+
24
+ headers.each { |k, v| request[k] = v } if headers
25
+
26
+ request
27
+ end
28
+
29
+ def self.build_base_request(method, path)
30
+ case method.to_s.downcase
31
+ when 'get'
32
+ Net::HTTP::Get.new(path)
33
+ when 'post'
34
+ Net::HTTP::Post.new(path)
35
+ when 'put'
36
+ Net::HTTP::Put.new(path)
37
+ when 'delete'
38
+ Net::HTTP::Delete.new(path)
39
+ when 'patch'
40
+ Net::HTTP::Patch.new(path)
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ require_relative 'remote_client'
2
+ require_relative 'remote_connector'
3
+ require_relative 'operation_definer'
4
+
5
+ module Restool
6
+ module Service
7
+ class RestoolService
8
+ include Restool::Service::OperationDefiner
9
+
10
+ def initialize(service_config, response_handler)
11
+ @service_config = service_config
12
+ @response_handler = response_handler
13
+ @remote_client = Restool::Service::RemoteClient.new(service_config.host, service_config.verify_ssl,
14
+ service_config.persistent, service_config.timeout)
15
+
16
+ define_operations(
17
+ @service_config, method(:make_request), method(:make_request_with_uri_params)
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ # this methods are called directly from the client though the OperationDefiner
24
+
25
+ def make_request(operation, params, headers = {})
26
+ path = Restool::Service::UriUtils.build_path(operation)
27
+
28
+ Restool::Service::RemoteConnector.execute(
29
+ @remote_client, operation, path, params, headers, @response_handler,
30
+ @service_config.representations, @service_config.basic_auth
31
+ )
32
+ end
33
+
34
+ def make_request_with_uri_params(operation, uri_params_values, params, headers = {})
35
+ path = Restool::Service::UriUtils.build_path(operation, uri_params_values)
36
+
37
+ Restool::Service::RemoteConnector.execute(
38
+ @remote_client, operation, path, params, headers, @response_handler,
39
+ @service_config.representations, @service_config.basic_auth
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ module Restool
2
+ module Service
3
+ module UriUtils
4
+
5
+ def self.build_path(operation, uri_params_values = nil)
6
+ path = operation.path
7
+
8
+ if uri_params_values
9
+ operation.uri_params.each_with_index do |uri_param, i|
10
+ path.sub!(/#{uri_param}/, uri_params_values[i].to_s)
11
+ end
12
+ end
13
+
14
+ path
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,136 @@
1
+ require 'yaml'
2
+
3
+ require_relative 'models'
4
+
5
+ module Restool
6
+ module Settings
7
+ module Loader
8
+ include Restool::Settings::Models
9
+
10
+ DEFAULT_TIMEOUT = 60
11
+ DEFAULT_SSL_VERIFY = false
12
+
13
+
14
+ def self.load(service_name)
15
+ service_config = config['services'].detect do |service|
16
+ service['name'] == service_name
17
+ end
18
+
19
+ raise "Service #{service_name} not found in configuration" unless service_config
20
+
21
+ build_service(service_config)
22
+ end
23
+
24
+ private
25
+
26
+ def self.build_service(service_config)
27
+ representations = if service_config['representations']
28
+ build_representations(service_config['representations'])
29
+ else
30
+ []
31
+ end
32
+
33
+ basic_auth = service_config['basic_auth'] || service_config['basic_authentication']
34
+ basic_auth = BasicAuthentication.new(basic_auth['user'], basic_auth['password']) if basic_auth
35
+
36
+ persistent_connection = service_config['persistent']
37
+ persistent_connection = if persistent_connection
38
+ PersistentConnection.new(
39
+ persistent_connection['pool_size'],
40
+ persistent_connection['warn_timeout'],
41
+ persistent_connection['force_retry'],
42
+ )
43
+ end
44
+
45
+ # Support host + common path in url config, e.g. api.com/v2/
46
+ paths_prefix_in_host = URI(service_config['url']).path
47
+
48
+ Models::Service.new(
49
+ service_config['name'],
50
+ service_config['url'],
51
+ service_config['operations'].map { |operation| build_operation(operation, paths_prefix_in_host) },
52
+ persistent_connection,
53
+ service_config['timeout'] || DEFAULT_TIMEOUT,
54
+ representations,
55
+ basic_auth,
56
+ service_config['ssl_verify'] || DEFAULT_SSL_VERIFY
57
+ )
58
+ end
59
+
60
+ def self.build_representations(representations)
61
+ representations_by_name = {}
62
+
63
+ representations.each do |representation|
64
+ fields = representation[1].map do |field|
65
+ RepresentationField.new(field['key'],
66
+ field['metonym'],
67
+ field['type'].to_sym)
68
+ end
69
+
70
+ representation = Representation.new(name = representation.first, fields)
71
+ representations_by_name[representation.name.to_sym] = representation
72
+ end
73
+
74
+ representations_by_name
75
+ end
76
+
77
+ def self.build_operation(operation_config, paths_prefix_in_host)
78
+ response = build_operation_response(operation_config['response']) if operation_config['response']
79
+
80
+ path = operation_config['path']
81
+ path = path[1..-1] if path[0] == '/'
82
+ paths_prefix_in_host.chomp!('/')
83
+
84
+ Operation.new(
85
+ operation_config['name'],
86
+ "#{paths_prefix_in_host}/#{path}",
87
+ operation_config['method'],
88
+ uri_params(operation_config),
89
+ response
90
+ )
91
+ end
92
+
93
+ def self.build_operation_response(response)
94
+ response_fields = response.map do |field|
95
+ OperationResponsField.new(field['key'], field['metonym'], field['type'].to_sym)
96
+ end
97
+
98
+ OperationResponse.new(response_fields)
99
+ end
100
+
101
+ def self.uri_params(operation_config)
102
+ operation_config['path'].scan(/:[a-zA-Z_]+[0-9]*[a-zA-Z_]*/)
103
+ end
104
+
105
+ def self.config
106
+ return @config if @config
107
+
108
+ files_to_load = Dir['config/restool/*'] + ['config/restool.yml', 'config/restool.json']
109
+
110
+ @config = { 'services' => [] }
111
+
112
+ files_to_load.each do |file_name|
113
+ next unless File.exist?(file_name)
114
+
115
+ extension = File.extname(file_name)
116
+
117
+ content = if extension == '.yml'
118
+ YAML.load_file(file_name)
119
+ elsif extension == '.json'
120
+ json_file = File.read(file_name)
121
+ JSON.parse(json_file)
122
+ end
123
+
124
+ @config['services'] += content['services']
125
+ end
126
+
127
+ @config
128
+ end
129
+
130
+ def self.validate
131
+ # TODO: perform validations
132
+ end
133
+
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,16 @@
1
+ module Restool
2
+ module Settings
3
+ module Models
4
+
5
+ Operation = Struct.new(:name, :path, :method, :uri_params, :response)
6
+ OperationResponse = Struct.new(:fields)
7
+ Service = Struct.new(:name, :host, :operations, :persistent, :timeout, :representations, :basic_auth, :verify_ssl)
8
+ Representation = Struct.new(:name, :fields)
9
+ RepresentationField = Struct.new(:key, :metonym, :type)
10
+ BasicAuthentication = Struct.new(:user, :password)
11
+ PersistentConnection = Struct.new(:respool_size, :want_timeout, :force_retry)
12
+ OperationResponsField = RepresentationField
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,91 @@
1
+ require 'bigdecimal'
2
+
3
+ require_relative 'object'
4
+
5
+ module Restool
6
+ module Traversal
7
+
8
+ TRAVERSAL_TYPE_STRING = :string
9
+ TRAVERSAL_TYPE_INTEGER = :integer
10
+ TRAVERSAL_TYPE_DECIMAL = :decimal
11
+ TRAVERSAL_TYPE_BOOLEAN = :boolean
12
+
13
+ TRAVERSAL_TYPES = [
14
+ TRAVERSAL_TYPE_STRING, TRAVERSAL_TYPE_INTEGER, TRAVERSAL_TYPE_DECIMAL, TRAVERSAL_TYPE_BOOLEAN
15
+ ]
16
+
17
+ module Converter
18
+
19
+ def self.convert(request_response, response_representation, representations)
20
+ object = Restool::Traversal::Object.new
21
+
22
+ if request_response.is_a?(Array)
23
+ request_response.map do |element|
24
+ map_response_to_representation(response_representation, element, object, representations)
25
+ end
26
+ else
27
+ map_response_to_representation(response_representation, request_response, object, representations)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def self.map_response_to_representation(representation, request_response, object, representations)
34
+ representation.fields.each do |field|
35
+ value = request_response[field.key]
36
+
37
+ object.class.__send__(:attr_accessor, var_name(field))
38
+
39
+ if Restool::Traversal::TRAVERSAL_TYPES.include?(field.type.to_sym)
40
+ map_primitive_field(value, field, object)
41
+ else
42
+ map_complex_field(value, field, object, representations)
43
+ end
44
+ end
45
+
46
+ object
47
+ end
48
+
49
+ def self.map_primitive_field(value, field, object)
50
+ new_value = if value.is_a?(Array)
51
+ value.map { |element| parse_value(field.type, element) }
52
+ else
53
+ parse_value(field.type, value)
54
+ end
55
+
56
+ object.__send__("#{var_name(field)}=", new_value)
57
+ end
58
+
59
+ def self.map_complex_field(value, field, object, representations)
60
+ operation_representation = representations[field.type.to_sym]
61
+
62
+ new_value = if value.is_a?(Array)
63
+ value.map { |element| convert(element, operation_representation, representations) }
64
+ else
65
+ convert(value, operation_representation, representations)
66
+ end
67
+
68
+ object.__send__("#{var_name(field)}=", new_value)
69
+ end
70
+
71
+ def self.var_name(field)
72
+ field.metonym || field.key
73
+ end
74
+
75
+ def self.parse_value(type, value)
76
+ case type
77
+ when Restool::Traversal::TRAVERSAL_TYPE_STRING
78
+ value
79
+ when Restool::Traversal::TRAVERSAL_TYPE_INTEGER
80
+ Integer(value)
81
+ when Restool::Traversal::TRAVERSAL_TYPE_DECIMAL
82
+ BigDecimal.new(scalar)
83
+ when Restool::Traversal::TRAVERSAL_TYPE_BOOLEAN
84
+ value.downcase == 'true'
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,17 @@
1
+ module Restool
2
+ module Traversal
3
+ class Object
4
+
5
+ def to_hash
6
+ instance_variables
7
+ .inject({}) do |acum, var|
8
+ acum[var.to_s.delete('@')] = instance_variable_get(var)
9
+ acum
10
+ end
11
+ end
12
+
13
+ alias_method :to_h, :to_hash
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Restool
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: restool
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Juan Andres Zeni
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: persistent_http
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ description: Make HTTP requests and handle its responses using simple method calls
28
+ Restool turns your HTTP API and its responses into Ruby interfaces.
29
+ email:
30
+ - juanandreszeni@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - lib/restool.rb
36
+ - lib/restool/service/operation_definer.rb
37
+ - lib/restool/service/remote_client.rb
38
+ - lib/restool/service/remote_connector.rb
39
+ - lib/restool/service/request_utils.rb
40
+ - lib/restool/service/restool_service.rb
41
+ - lib/restool/service/uri_utils.rb
42
+ - lib/restool/settings/loader.rb
43
+ - lib/restool/settings/models.rb
44
+ - lib/restool/traversal/converter.rb
45
+ - lib/restool/traversal/object.rb
46
+ - lib/restool/version.rb
47
+ homepage: https://github.com/jzeni/restool
48
+ licenses:
49
+ - MIT
50
+ metadata: {}
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 2.3.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.0.1
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Turn your API and its responses into Ruby interfaces.
70
+ test_files: []