restool 0.1.0

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 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: []