setsuzoku 0.10.1

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.
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