setsuzoku 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +7 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +82 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +93 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/lib/setsuzoku.rb +37 -0
  15. data/lib/setsuzoku/api_response.rb +11 -0
  16. data/lib/setsuzoku/api_strategy.rb +124 -0
  17. data/lib/setsuzoku/auth_strategy.rb +72 -0
  18. data/lib/setsuzoku/credential.rb +37 -0
  19. data/lib/setsuzoku/exception.rb +18 -0
  20. data/lib/setsuzoku/external_api_handler.rb +19 -0
  21. data/lib/setsuzoku/pluggable.rb +87 -0
  22. data/lib/setsuzoku/plugin.rb +128 -0
  23. data/lib/setsuzoku/rspec.rb +2 -0
  24. data/lib/setsuzoku/rspec/dynamic_spec_helper.rb +281 -0
  25. data/lib/setsuzoku/service.rb +70 -0
  26. data/lib/setsuzoku/service/web_service.rb +21 -0
  27. data/lib/setsuzoku/service/web_service/api_strategies/rest_api_request.rb +17 -0
  28. data/lib/setsuzoku/service/web_service/api_strategies/rest_strategy.rb +155 -0
  29. data/lib/setsuzoku/service/web_service/api_strategy.rb +169 -0
  30. data/lib/setsuzoku/service/web_service/auth_strategies/basic_auth_strategy.rb +50 -0
  31. data/lib/setsuzoku/service/web_service/auth_strategies/o_auth_strategy.rb +173 -0
  32. data/lib/setsuzoku/service/web_service/auth_strategy.rb +48 -0
  33. data/lib/setsuzoku/service/web_service/credentials/basic_auth_credential.rb +52 -0
  34. data/lib/setsuzoku/service/web_service/credentials/o_auth_credential.rb +83 -0
  35. data/lib/setsuzoku/service/web_service/service.rb +31 -0
  36. data/lib/setsuzoku/utilities.rb +7 -0
  37. data/lib/setsuzoku/version.rb +6 -0
  38. data/setsuzoku.gemspec +50 -0
  39. data/sorbet/config +2 -0
  40. data/sorbet/rbi/gems/activesupport.rbi +1125 -0
  41. data/sorbet/rbi/gems/addressable.rbi +199 -0
  42. data/sorbet/rbi/gems/concurrent-ruby.rbi +1586 -0
  43. data/sorbet/rbi/gems/crack.rbi +62 -0
  44. data/sorbet/rbi/gems/faraday.rbi +615 -0
  45. data/sorbet/rbi/gems/hashdiff.rbi +66 -0
  46. data/sorbet/rbi/gems/httparty.rbi +401 -0
  47. data/sorbet/rbi/gems/i18n.rbi +133 -0
  48. data/sorbet/rbi/gems/mime-types-data.rbi +17 -0
  49. data/sorbet/rbi/gems/mime-types.rbi +218 -0
  50. data/sorbet/rbi/gems/multi_xml.rbi +35 -0
  51. data/sorbet/rbi/gems/multipart-post.rbi +53 -0
  52. data/sorbet/rbi/gems/nokogiri.rbi +1011 -0
  53. data/sorbet/rbi/gems/public_suffix.rbi +104 -0
  54. data/sorbet/rbi/gems/rake.rbi +646 -0
  55. data/sorbet/rbi/gems/rspec-core.rbi +1893 -0
  56. data/sorbet/rbi/gems/rspec-expectations.rbi +1123 -0
  57. data/sorbet/rbi/gems/rspec-mocks.rbi +1090 -0
  58. data/sorbet/rbi/gems/rspec-support.rbi +280 -0
  59. data/sorbet/rbi/gems/rspec.rbi +15 -0
  60. data/sorbet/rbi/gems/safe_yaml.rbi +124 -0
  61. data/sorbet/rbi/gems/thread_safe.rbi +82 -0
  62. data/sorbet/rbi/gems/tzinfo.rbi +406 -0
  63. data/sorbet/rbi/gems/webmock.rbi +532 -0
  64. data/sorbet/rbi/hidden-definitions/hidden.rbi +13722 -0
  65. data/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +1431 -0
  66. data/sorbet/rbi/sorbet-typed/lib/bundler/all/bundler.rbi +8684 -0
  67. data/sorbet/rbi/sorbet-typed/lib/httparty/all/httparty.rbi +427 -0
  68. data/sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi +108 -0
  69. data/sorbet/rbi/sorbet-typed/lib/ruby/all/open3.rbi +111 -0
  70. data/sorbet/rbi/sorbet-typed/lib/ruby/all/resolv.rbi +543 -0
  71. data/sorbet/rbi/todo.rbi +11 -0
  72. metadata +255 -0
@@ -0,0 +1,70 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Setsuzoku
5
+ # Core service required for a web service plugin.
6
+ # It should be able to register all of its available api and auth strategies.
7
+ # It acts as the orchestration between a plugin, auth_strategy, and api_strategy.
8
+ module Service
9
+ autoload :WebService, 'setsuzoku/service/web_service'
10
+
11
+ extend Forwardable
12
+ extend T::Sig
13
+ extend T::Helpers
14
+ abstract!
15
+
16
+ attr_accessor :plugin
17
+ attr_accessor :auth_strategy
18
+ attr_accessor :api_strategy
19
+ attr_accessor :external_api_handler
20
+ def_delegators :@auth_strategy, :new_credential!
21
+ def_delegators :@api_strategy, :call_external_api, :request_class
22
+
23
+ def self.included(klass)
24
+ klass.extend(ClassMethods)
25
+ end
26
+
27
+ module ClassMethods
28
+ extend T::Sig
29
+ extend T::Helpers
30
+ abstract!
31
+
32
+ # The api and auth strategies available for the service.
33
+ # api: { simple_name: ApiModule }
34
+ #
35
+ # @return [Hash(Hash(Class))] the available_strategies object for the Service.
36
+ sig { abstract.returns(T::Hash[Symbol, T::Hash[Symbol, Class]]) }
37
+ def available_strategies; end
38
+ end
39
+
40
+ # Initialize the plugin_service by setting its auth and api strategies.
41
+ #
42
+ # @param auth_strategy [AuthStrategy] the auth_strategy for the service to use.
43
+ # @param api_strategy [ApiStrategy] the api_strategy for the service to use.
44
+ #
45
+ # @return [Service] the new instance of service with its correct strategies.
46
+ sig(:final) do
47
+ params(
48
+ plugin: T.untyped,
49
+ strategies: T::Hash[Symbol, T.untyped],
50
+ args: T.untyped
51
+ ).returns(T.any(Setsuzoku::Service::WebService::Service, T.untyped))
52
+ end
53
+ def initialize(plugin:, strategies:, **args)
54
+ #TODO: here we need to assign credentials etc, I think.
55
+ self.plugin = plugin
56
+ self.external_api_handler = Setsuzoku.external_api_handler.new
57
+
58
+ # iterate over all strategies this plugin's service uses and set their configuration
59
+ strategies.each do |strategy, attrs|
60
+ # get the strategy type from auth_strategy/api_strategy etc.
61
+ type = T.must(strategy.to_s.split("_strategy").first).to_sym
62
+ strat = attrs[:strategy]
63
+ # associate the strategy with the service
64
+ self.send("#{strategy}=", self.class.available_strategies[type][strat].new(service: self, credential: args[:credential], **attrs.except(:strategy)))
65
+ end
66
+
67
+ self
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'setsuzoku/service/web_service/api_strategy'
5
+ require 'setsuzoku/service/web_service/auth_strategy'
6
+ require 'setsuzoku/service/web_service/service'
7
+ require 'setsuzoku/service/web_service/api_strategies/rest_strategy'
8
+ require 'setsuzoku/service/web_service/api_strategies/rest_api_request'
9
+ require 'setsuzoku/service/web_service/auth_strategies/o_auth_strategy'
10
+ require 'setsuzoku/service/web_service/auth_strategies/basic_auth_strategy'
11
+ require 'setsuzoku/service/web_service/credentials/o_auth_credential'
12
+ require 'setsuzoku/service/web_service/credentials/basic_auth_credential'
13
+
14
+ module Setsuzoku
15
+ module Service
16
+ module WebService
17
+ # Core service required for a web service plugin.
18
+ # It should be able to register all of its available api and auth strategies.
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+
2
+ # typed: false
3
+ # frozen_string_literal: true
4
+
5
+ module Setsuzoku
6
+ module Service
7
+ module WebService
8
+ module ApiStrategies
9
+ class RestAPIRequest < T::Struct
10
+ prop :action, Symbol, default: nil
11
+ prop :body, T::Hash[T.untyped, T.untyped], default: {}
12
+ prop :without_headers, T::Boolean, default: false
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,155 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require 'active_support/json'
5
+ require 'active_support/core_ext/hash/indifferent_access'
6
+ require 'active_support/core_ext/string/access'
7
+
8
+ module Setsuzoku
9
+ module Service
10
+ module WebService
11
+ module ApiStrategies
12
+ # Defines all necessary methods for handling interfacing with a REST API.
13
+ class RestStrategy < WebService::ApiStrategy
14
+ extend T::Sig
15
+ extend T::Helpers
16
+
17
+ def request_class
18
+ RestAPIRequest
19
+ end
20
+
21
+ def self.required_instance_methods
22
+ []
23
+ end
24
+
25
+ # Make a REST API request.
26
+ # 1. Format the request and send it via the appropriate HTTP method.
27
+ # 2. Format the response and return it.
28
+ #
29
+ # @param request [RestAPIRequest] the constructed API request object.
30
+ # @param action_details [Hash] the action details for the action to execute.
31
+ # @param options [Any] additional options needed to pass to correctly perform the request.
32
+ # options are:
33
+ # media_type - 'json'
34
+ #
35
+ # @return [Hash] the parsed response object.
36
+ sig { override.params(request: T.untyped, action_details: T::Hash[T.untyped, T.untyped], options: T.untyped).returns(Faraday::Response) }
37
+ def perform_external_call(request:, action_details:, **options)
38
+ request_properties = self.get_request_properties(action_name: request.action, action_details: action_details, req_params: request.body)
39
+ request_options = self.request_options(request_properties[:request_format], action_details[:actions][request.action])
40
+
41
+ full_request = self.formulate_request(request_properties, request_options)
42
+
43
+ @faraday = Faraday.new(url: request_properties[:formatted_full_url]) do |faraday|
44
+ faraday.request(:multipart) if options[:attachments].present?
45
+ faraday.request(:url_encoded)
46
+ # faraday.basic_auth(request_options[:basic_auth][:username], request_options[:basic_auth][:password]) if request_options[:basic_auth]
47
+ faraday.request(:basic_auth, request_options[:basic_auth][:username], request_options[:basic_auth][:password]) if request_options[:basic_auth]
48
+ # faraday.response request_properties[:response_format] if request_properties[:response_format]
49
+ faraday.authorization(:Bearer, request_properties[:token]) if request_properties[:token]
50
+ # request_options[:headers][:Authorization].prepend('Bearer ') if request_options[:headers][:Authorization]
51
+ faraday.adapter Faraday.default_adapter
52
+ end
53
+
54
+ if options[:attachments].present?
55
+ resp = @faraday.post do |req|
56
+ io = StringIO.new(full_request)
57
+ payload = {}
58
+ payload[:json] = Faraday::UploadIO.new(io, 'application/json')
59
+ payload[:attachment] = options[:attachments].map{ |file| Faraday::UploadIO.new(file[0], file[1], file[2]) }
60
+ req.body = payload
61
+ end
62
+ else
63
+ @faraday.headers = request_options[:headers] if request_options[:headers]
64
+ resp = @faraday.send(request_properties[:request_method], request_properties[:formatted_full_url], full_request)
65
+ end
66
+ resp
67
+ end
68
+
69
+ sig do
70
+ params(
71
+ action_name: Symbol,
72
+ for_stub: T::Boolean,
73
+ req_params: T::Hash[T.untyped, T.untyped],
74
+ action_details: T::Hash[Symbol, T.untyped]
75
+ ).returns(T::Hash[Symbol, T.untyped])
76
+ end
77
+
78
+ def get_request_properties(action_name:, for_stub: false, req_params: {}, action_details: { actions: self.plugin.api_actions, url: self.plugin.api_base_url })
79
+ action = action_details[:actions][action_name]
80
+ request_method, endpoint = action.first
81
+ request_method = request_method.downcase.to_sym
82
+ request_format = action[:request_type]
83
+ response_format = action[:response_type]
84
+ token = action[:token]
85
+ stub_data = action[:stub_data] if for_stub
86
+ full_url = action_details[:url] + endpoint
87
+ formatted_full_url, req_params = self.replace_dynamic_vars(full_url: full_url, req_params: req_params)
88
+ {
89
+ request_method: request_method,
90
+ endpoint: endpoint,
91
+ request_format: request_format,
92
+ response_format: response_format,
93
+ formatted_full_url: formatted_full_url,
94
+ req_params: req_params,
95
+ stub_data: stub_data,
96
+ token: token
97
+ }
98
+ end
99
+
100
+ # Create the proper request body based on the request_properties and request_options passed
101
+ #
102
+ # @param request_properties [Hash] information pertaining to the body of the request
103
+ # @param request_options [Hash] information pertainint to the headers of the request
104
+ #
105
+ # @return full_request [Hash/String] returns the request body in the format required
106
+ def formulate_request(request_properties = {}, request_options = {})
107
+ request_format = request_properties.dig(:request_format).to_s
108
+ content_type = request_options.dig(:headers, :'Content-Type').to_s
109
+
110
+ if request_properties[:request_method] == :get || request_properties[:req_params].empty?
111
+ request_properties[:req_params]
112
+ else
113
+ # if the header or request format and request format include urlencoded return the body as a hash
114
+ if request_format.include?('urlencoded') && content_type.include?('urlencoded')
115
+ request_properties[:req_params]
116
+ else
117
+ # return either xml or json
118
+ if request_properties[:request_format] == :xml
119
+ convert_hash_to_xml(request_properties[:req_params])
120
+ else
121
+ request_properties[:req_params].to_json
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def request_options(request_format = nil, action_details = {})
128
+ request_options = self.auth_strategy.auth_headers.merge(self.plugin.api_headers).merge(action_details[:request_options] || {})
129
+ (request_options[:headers] ||= {})[:'Content-Type'] = "application/#{request_format || :json}"
130
+ request_options
131
+ end
132
+
133
+ def replace_dynamic_vars(full_url:, req_params: {})
134
+ # replace matching vars in the action with matching params.
135
+ # scans the string for variables like {{number_sid}} and replaces it with the matching key in params
136
+ # removes the params variable, as it's probably intended to be in the url only. If you encounter a need to have
137
+ # it in the body and the url, then you should put it in params twice with different names.
138
+ req_params = req_params.dup
139
+ full_url = full_url.dup
140
+ dynamic_vars = self.plugin.dynamic_url_params.merge(req_params).with_indifferent_access
141
+ full_url.scan(/({{.*?}})/).flatten.each do |var|
142
+ var_name = var.tr('{{ }}', '')
143
+ next unless dynamic_vars[var_name]
144
+
145
+ full_url.gsub!(var, dynamic_vars[var_name])
146
+ req_params.delete(var_name)
147
+ req_params.delete(var_name.to_sym)
148
+ end
149
+ [full_url, req_params]
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,169 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Setsuzoku
5
+ module Service
6
+ # Defines all common methods for handling interfacing with a Web Service API.
7
+ module WebService
8
+
9
+ class ApiStrategy
10
+ include Setsuzoku::ApiStrategy
11
+ extend T::Sig
12
+ extend T::Helpers
13
+
14
+ # These are interface methods that a plugin that implements this strategy must implement.
15
+ module InterfaceMethods
16
+ extend T::Sig
17
+ extend T::Helpers
18
+ interface!
19
+
20
+ # The base Web Service API URL for the plugin.
21
+ #
22
+ # @return [String] the API base url.
23
+ sig { abstract.returns(String) }
24
+ def api_base_url; end
25
+
26
+ # the base webhook api url
27
+ #
28
+ # @return [String] the webhook base url
29
+ def webhook_base_url; end
30
+
31
+ # The collection of all implemented endpoints for the API.
32
+ # Formatted as: { api_action: { 'HTTP_VERB': 'api.com/endpoints' } }
33
+ #
34
+ # @return [Hash] all endpoint definitions for the API.
35
+ sig { abstract.returns(T::Hash[T.untyped, T.untyped]) }
36
+ def api_actions; end
37
+
38
+ # Any api request headers that this service needs to set.
39
+ #
40
+ # @return [Hash]
41
+ sig { abstract.returns(T::Hash[Symbol, T.untyped]) }
42
+ def api_headers; end
43
+
44
+ # All dynamic url parameters provided by the plugin.
45
+ #
46
+ # @return [Hash(String)] all parameters that need to be replaced dynamically for url requests.
47
+ sig { abstract.returns(T::Hash[Symbol, T.nilable(String)]) }
48
+ def dynamic_url_params; end
49
+ end
50
+
51
+ # Perform the external call for the external API.
52
+ # Each WebService::ApiStrategy must define how this works.
53
+ #
54
+ # It should:
55
+ # 1. Make the external request
56
+ # 2. Parse the response
57
+ # 3. Handle any bad response
58
+ # 4. Format the response
59
+ #
60
+ # @param request [APIRequest] the request object to be used for the request. Each strategy defines its request structure.
61
+ # @param action_details [Hash] the action_details for the action to execute.
62
+ # @param options [Any] any additional options needed to pass to correctly perform the request.
63
+ #
64
+ # @return [Any] the formatted response object.
65
+ sig { override.params(request: T.untyped, action_details: T::Hash[T.untyped, T.untyped], options: T.untyped).returns(T.untyped) }
66
+ def perform_external_call(request:, action_details:, **options); end
67
+
68
+ # Parse the response from the API for the given request.
69
+ # This should just convert JSON strings/XML/SQL rows to a formatted response Hash.
70
+ #
71
+ # @param response [HTTParty::Response] the response from the HTTP request.
72
+ # @param options [Hash] the parsing options. Generally the response_type. e.g. :xml, :json
73
+ #
74
+ # @return [Hash] the parsed hash of the response object.
75
+ sig { override.params(response: Faraday::Response, options: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
76
+ def parse_response(response:, **options)
77
+ case options[:response_type]
78
+ when :json
79
+ JSON.parse(response.body).deep_symbolize_keys
80
+ when :xml
81
+ convert_xml_to_hash(response.body)
82
+ else
83
+ JSON.parse(response.body).deep_symbolize_keys
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ sig { params(hash: T::Hash[Symbol, T.untyped], mutate_keys: T::Boolean).returns(String)}
90
+ def convert_hash_to_xml(hash, mutate_keys = true)
91
+ hash = hash.map do |k, v|
92
+ text = if v.is_a?(Hash)
93
+ convert_hash_to_xml(v)
94
+ elsif v.is_a?(Array)
95
+ v.map do |elem|
96
+ convert_hash_to_xml(elem)
97
+ end.join
98
+ else
99
+ v
100
+ end
101
+ k = k.to_s.camelize if mutate_keys
102
+ "<%s>%s</%s>" % [k, text, k]
103
+ end.join
104
+ hash
105
+ end
106
+
107
+ # Convert an XML string to a usable hash.
108
+ sig { params(xml: String).returns(T::Hash[Symbol, T.untyped])}
109
+ def convert_xml_to_hash(xml)
110
+ begin
111
+ result = Nokogiri::XML(xml)
112
+ return { result.root.name.underscore.to_sym => xml_node_to_hash(result.root)}
113
+ rescue Exception => e
114
+ {}
115
+ end
116
+ end
117
+
118
+ def xml_node_to_hash(node)
119
+ # If we are at the root of the document, start the hash
120
+ if node.element?
121
+ result_hash = {}
122
+ if node.attributes != {}
123
+ attributes = {}
124
+ node.attributes.keys.each do |key|
125
+ attributes[node.attributes[key].name.underscore.to_sym] = node.attributes[key].value
126
+ end
127
+ end
128
+ if node.children.size > 0
129
+ node.children.each do |child|
130
+ result = xml_node_to_hash(child)
131
+
132
+ if child.name == "text"
133
+ unless child.next_sibling || child.previous_sibling
134
+ return result unless attributes
135
+ result_hash[child.name.underscore.to_sym] = result
136
+ end
137
+ elsif result_hash[child.name.to_sym]
138
+
139
+ if result_hash[child.name.to_sym].is_a?(Array)
140
+ result_hash[child.name.underscore.to_sym] << result
141
+ else
142
+ result_hash[child.name.underscore.to_sym] = [result_hash[child.name.to_sym]] << result
143
+ end
144
+ else
145
+ stripped_children = node.children.reject{ |n| n.text.strip.blank? }
146
+ if stripped_children.length > 1 && stripped_children.combination(2).all? { |a,b| a.name == b.name }
147
+ return result_hash[node.name.underscore.to_sym] = stripped_children.map{|n| xml_node_to_hash(n) }
148
+ else
149
+ result_hash[child.name.underscore.to_sym] = result
150
+ end
151
+ end
152
+ end
153
+ if attributes
154
+ #add code to remove non-data attributes e.g. xml schema, namespace here
155
+ #if there is a collision then node content supersets attributes
156
+ result_hash = attributes.merge(result_hash)
157
+ end
158
+ return result_hash
159
+ else
160
+ return attributes
161
+ end
162
+ else
163
+ return node.content.to_s
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,50 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ module Setsuzoku
5
+ module Service
6
+ module WebService
7
+ module AuthStrategies
8
+ # The API OAuth Authentication Interface definition.
9
+ # Any Plugin that implements this must implement all methods required for OAuth.
10
+ #
11
+ # Defines all necessary methods for handling authentication for any authentication strategy.
12
+ class BasicAuthStrategy < WebService::AuthStrategy
13
+ extend T::Sig
14
+ extend T::Helpers
15
+
16
+ def self.required_instance_methods
17
+ []
18
+ end
19
+
20
+ def self.credential_class
21
+ Setsuzoku::Service::WebService::Credentials::BasicAuthCredential
22
+ end
23
+
24
+ # OAuth API authentication headers.
25
+ #
26
+ # @return [Hash(String)] the formatted authorization headers for an OAuth request
27
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
28
+ def auth_headers
29
+ {
30
+ basic_auth: {
31
+ username: self.credential.username,
32
+ password: self.credential.password
33
+ }
34
+ }
35
+ end
36
+
37
+
38
+ # if the auth credientials are valid
39
+ #
40
+ #
41
+ # sig { override.returns(T::Boolean) }
42
+ def auth_credential_valid?
43
+ true
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end