workato-connector-sdk 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +20 -0
  3. data/README.md +1093 -0
  4. data/exe/workato +5 -0
  5. data/lib/workato/cli/edit_command.rb +71 -0
  6. data/lib/workato/cli/exec_command.rb +191 -0
  7. data/lib/workato/cli/generate_command.rb +105 -0
  8. data/lib/workato/cli/generators/connector_generator.rb +94 -0
  9. data/lib/workato/cli/generators/master_key_generator.rb +56 -0
  10. data/lib/workato/cli/main.rb +142 -0
  11. data/lib/workato/cli/push_command.rb +208 -0
  12. data/lib/workato/connector/sdk/account_properties.rb +60 -0
  13. data/lib/workato/connector/sdk/action.rb +88 -0
  14. data/lib/workato/connector/sdk/block_invocation_refinements.rb +30 -0
  15. data/lib/workato/connector/sdk/connector.rb +230 -0
  16. data/lib/workato/connector/sdk/dsl/account_property.rb +15 -0
  17. data/lib/workato/connector/sdk/dsl/call.rb +17 -0
  18. data/lib/workato/connector/sdk/dsl/error.rb +15 -0
  19. data/lib/workato/connector/sdk/dsl/http.rb +60 -0
  20. data/lib/workato/connector/sdk/dsl/lookup_table.rb +15 -0
  21. data/lib/workato/connector/sdk/dsl/time.rb +21 -0
  22. data/lib/workato/connector/sdk/dsl/workato_code_lib.rb +105 -0
  23. data/lib/workato/connector/sdk/dsl/workato_schema.rb +15 -0
  24. data/lib/workato/connector/sdk/dsl.rb +46 -0
  25. data/lib/workato/connector/sdk/errors.rb +30 -0
  26. data/lib/workato/connector/sdk/lookup_tables.rb +62 -0
  27. data/lib/workato/connector/sdk/object_definitions.rb +74 -0
  28. data/lib/workato/connector/sdk/operation.rb +217 -0
  29. data/lib/workato/connector/sdk/request.rb +399 -0
  30. data/lib/workato/connector/sdk/settings.rb +130 -0
  31. data/lib/workato/connector/sdk/summarize.rb +61 -0
  32. data/lib/workato/connector/sdk/trigger.rb +96 -0
  33. data/lib/workato/connector/sdk/version.rb +9 -0
  34. data/lib/workato/connector/sdk/workato_schemas.rb +37 -0
  35. data/lib/workato/connector/sdk/xml.rb +35 -0
  36. data/lib/workato/connector/sdk.rb +58 -0
  37. data/lib/workato/extension/array.rb +124 -0
  38. data/lib/workato/extension/case_sensitive_headers.rb +51 -0
  39. data/lib/workato/extension/currency.rb +15 -0
  40. data/lib/workato/extension/date.rb +14 -0
  41. data/lib/workato/extension/enumerable.rb +55 -0
  42. data/lib/workato/extension/extra_chain_cert.rb +40 -0
  43. data/lib/workato/extension/hash.rb +13 -0
  44. data/lib/workato/extension/integer.rb +17 -0
  45. data/lib/workato/extension/nil_class.rb +17 -0
  46. data/lib/workato/extension/object.rb +38 -0
  47. data/lib/workato/extension/phone.rb +14 -0
  48. data/lib/workato/extension/string.rb +268 -0
  49. data/lib/workato/extension/symbol.rb +13 -0
  50. data/lib/workato/extension/time.rb +13 -0
  51. data/lib/workato/testing/vcr_encrypted_cassette_serializer.rb +38 -0
  52. data/lib/workato/testing/vcr_multipart_body_matcher.rb +32 -0
  53. data/lib/workato-connector-sdk.rb +3 -0
  54. data/templates/.rspec.erb +3 -0
  55. data/templates/Gemfile.erb +10 -0
  56. data/templates/connector.rb.erb +37 -0
  57. data/templates/spec/action_spec.rb.erb +36 -0
  58. data/templates/spec/connector_spec.rb.erb +18 -0
  59. data/templates/spec/method_spec.rb.erb +13 -0
  60. data/templates/spec/object_definition_spec.rb.erb +18 -0
  61. data/templates/spec/pick_list_spec.rb.erb +13 -0
  62. data/templates/spec/spec_helper.rb.erb +38 -0
  63. data/templates/spec/trigger_spec.rb.erb +61 -0
  64. metadata +372 -0
@@ -0,0 +1,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+ require 'rest-client'
5
+ require 'json'
6
+ require 'gyoku'
7
+ require 'net/http'
8
+ require 'net/http/digest_auth'
9
+
10
+ require_relative './block_invocation_refinements'
11
+
12
+ module Workato
13
+ module Connector
14
+ module Sdk
15
+ class Request < SimpleDelegator
16
+ using BlockInvocationRefinements
17
+
18
+ def initialize(uri, method: 'GET', connection: {}, settings: {}, action: nil)
19
+ super(nil)
20
+ @uri = uri
21
+ @authorization = (connection[:authorization] || {}).with_indifferent_access
22
+ @settings = settings
23
+ @base_uri = connection[:base_uri]&.call(settings.with_indifferent_access)
24
+ @method = method
25
+ @action = action
26
+ @headers = {}
27
+ @case_sensitive_headers = {}
28
+ @params = {}.with_indifferent_access
29
+ @render_request = ->(payload) { payload }
30
+ @parse_response = ->(payload) { payload }
31
+ @after_response = ->(_response_code, parsed_response, _response_headers) { parsed_response }
32
+ end
33
+
34
+ def method_missing(*args, &block)
35
+ execute!.send(*args, &block)
36
+ end
37
+
38
+ def execute!
39
+ __getobj__ || __setobj__(
40
+ authorized do
41
+ begin
42
+ request = build_request
43
+ response = execute(request)
44
+ rescue RestClient::Unauthorized => e
45
+ Kernel.raise e unless @digest_auth
46
+
47
+ @digest_auth = false
48
+ headers('Authorization' => Net::HTTP::DigestAuth.new.auth_header(
49
+ URI.parse(build_url),
50
+ e.response.headers[:www_authenticate],
51
+ method.to_s.upcase
52
+ ))
53
+ request = build_request
54
+ response = execute(request)
55
+ end
56
+ detect_error!(response.body)
57
+ parsed_response = @parse_response.call(response)
58
+ detect_error!(parsed_response)
59
+ within_action_context(response.code, parsed_response, response.headers, &@after_response)
60
+ end
61
+ )
62
+ rescue RestClient::Exception => e
63
+ if after_error_response_matches?(e)
64
+ return apply_after_error_response(e)
65
+ end
66
+
67
+ Kernel.raise RequestError.new(response: e.response, message: e.message, method: current_verb,
68
+ code: e.http_code)
69
+ end
70
+
71
+ def headers(headers)
72
+ @headers.merge!(headers)
73
+ self
74
+ end
75
+
76
+ def case_sensitive_headers(headers)
77
+ @case_sensitive_headers.merge!(headers)
78
+ self
79
+ end
80
+
81
+ def params(params)
82
+ @params.merge!(params)
83
+ self
84
+ end
85
+
86
+ def payload(payload = nil)
87
+ case payload
88
+ when Array
89
+ @payload ||= []
90
+ @payload += payload
91
+ when NilClass
92
+ # no-op
93
+ else
94
+ @payload ||= {}.with_indifferent_access
95
+ @payload.merge!(payload)
96
+ end
97
+ yield(@payload) if Kernel.block_given?
98
+ self
99
+ end
100
+
101
+ def user(usr)
102
+ @user = usr
103
+ self
104
+ end
105
+
106
+ def password(pwd)
107
+ @password = pwd
108
+ self
109
+ end
110
+
111
+ def digest_auth
112
+ @digest_auth = true
113
+ self
114
+ end
115
+
116
+ def follow_redirection
117
+ @follow_redirection = true
118
+ self
119
+ end
120
+
121
+ def ignore_redirection
122
+ @follow_redirection = false
123
+ self
124
+ end
125
+
126
+ def after_response(&after_response)
127
+ @after_response = after_response
128
+ self
129
+ end
130
+
131
+ def after_error_response(*matches, &after_error_response)
132
+ @after_error_response_matches = matches
133
+ @after_error_response = after_error_response
134
+ self
135
+ end
136
+
137
+ def format_json
138
+ request_format_json.response_format_json
139
+ end
140
+
141
+ def request_format_json
142
+ @content_type_header = :json
143
+ @render_request = ->(payload) { ActiveSupport::JSON.encode(payload) if payload }
144
+ self
145
+ end
146
+
147
+ def response_format_json
148
+ @accept_header = :json
149
+ @parse_response = ->(payload) { ActiveSupport::JSON.decode(payload.presence || '{}') }
150
+ self
151
+ end
152
+
153
+ def format_xml(root_element_name, namespaces = {}, **options)
154
+ request_format_xml(root_element_name, namespaces).response_format_xml(**options)
155
+ end
156
+
157
+ def request_format_xml(root_element_name, namespaces = {})
158
+ @content_type_header = :xml
159
+ @render_request = Kernel.lambda { |payload|
160
+ next unless payload
161
+
162
+ Gyoku.xml({ root_element_name => payload.merge(namespaces).deep_symbolize_keys }, key_converter: :none)
163
+ }
164
+ self
165
+ end
166
+
167
+ def response_format_xml(strip_response_namespaces: false)
168
+ @accept_header = :xml
169
+ @parse_response = ->(payload) { Xml.parse_xml_to_hash(payload, strip_namespaces: strip_response_namespaces) }
170
+ self
171
+ end
172
+
173
+ def request_body(body)
174
+ @payload = body
175
+ @render_request = ->(payload) { payload }
176
+ self
177
+ end
178
+
179
+ def response_format_raw
180
+ @parse_response = Kernel.lambda do |payload|
181
+ payload.body.force_encoding(::Encoding::BINARY)
182
+ payload.body.valid_encoding? ? payload.body : payload.body.force_encoding(::Encoding::BINARY)
183
+ end
184
+ self
185
+ end
186
+
187
+ def request_format_multipart_form
188
+ @content_type_header = nil
189
+
190
+ @render_request = Kernel.lambda do |payload|
191
+ payload&.each_with_object({}) do |(name, (value, content_type, original_filename)), rendered|
192
+ rendered[name] = if content_type.present?
193
+ Part.new(name, content_type, original_filename || ::File.basename(name), value.to_s)
194
+ else
195
+ value
196
+ end
197
+ end&.merge!(multipart: true) || {}
198
+ end
199
+
200
+ self
201
+ end
202
+
203
+ def request_format_www_form_urlencoded
204
+ @content_type_header = 'application/x-www-form-urlencoded'
205
+ @render_request = Kernel.lambda { |payload| payload.to_param }
206
+ self
207
+ end
208
+
209
+ def current_verb
210
+ method
211
+ end
212
+
213
+ def current_url
214
+ build_url
215
+ end
216
+
217
+ def auth_type(auth_type)
218
+ @auth_type = auth_type
219
+ self
220
+ end
221
+
222
+ def tls_client_cert(certificate:, key:, passphrase: nil, intermediates: [])
223
+ @ssl_client_cert = OpenSSL::X509::Certificate.new(certificate)
224
+ @ssl_client_key = OpenSSL::PKey::RSA.new(key, passphrase)
225
+ @ssl_client_intermediate_certs = Array.wrap(intermediates).compact.map do |intermediate|
226
+ OpenSSL::X509::Certificate.new(intermediate)
227
+ end
228
+ self
229
+ end
230
+
231
+ def tls_server_certs(certificates:, strict: true)
232
+ @ssl_cert_store ||= OpenSSL::X509::Store.new
233
+ @ssl_cert_store.set_default_paths unless strict
234
+ Array.wrap(certificates).each do |certificate|
235
+ @ssl_cert_store.add_cert(OpenSSL::X509::Certificate.new(certificate))
236
+ end
237
+ self
238
+ end
239
+
240
+ private
241
+
242
+ attr_reader :method
243
+
244
+ def execute(request)
245
+ if @follow_redirection.nil?
246
+ request.execute
247
+ else
248
+ request.execute do |res|
249
+ case res.code
250
+ when 301, 302, 307, 308
251
+ if @follow_redirection
252
+ res.follow_redirection
253
+ else
254
+ res
255
+ end
256
+ else
257
+ res.return!
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ def build_request
264
+ RestClient::Request.new(
265
+ {
266
+ method: method,
267
+ url: build_url,
268
+ headers: build_headers,
269
+ payload: @render_request.call(@payload)
270
+ }.tap do |request_hash|
271
+ if @ssl_client_cert.present? && @ssl_client_key.present?
272
+ request_hash[:ssl_client_cert] = @ssl_client_cert
273
+ request_hash[:ssl_client_key] = @ssl_client_key
274
+ end
275
+ request_hash[:ssl_cert_store] = @ssl_cert_store if @ssl_cert_store
276
+ end
277
+ ).tap do |request|
278
+ request.case_sensitive_headers = @case_sensitive_headers.transform_keys(&:to_s)
279
+ if @ssl_client_intermediate_certs.present? && @ssl_client_cert.present? && @ssl_client_key.present?
280
+ request.extra_chain_cert = @ssl_client_intermediate_certs
281
+ end
282
+ end
283
+ end
284
+
285
+ def build_url
286
+ uri = if @base_uri
287
+ URI.parse(@base_uri).merge(@uri)
288
+ else
289
+ URI.parse(@uri)
290
+ end
291
+
292
+ return uri.to_s unless @params.any? || @user || @password
293
+
294
+ unless @digest_auth
295
+ uri.user = URI.encode_www_form_component(@user) if @user
296
+ uri.password = URI.encode_www_form_component(@password) if @password
297
+ end
298
+
299
+ return uri.to_s unless @params.any?
300
+
301
+ query = uri.query.to_s.split('&').select(&:present?).join('&').presence
302
+ params = @params.to_param.presence
303
+ if query && params
304
+ uri.query = "#{query}&#{params}"
305
+ elsif params
306
+ uri.query = params
307
+ end
308
+
309
+ uri.to_s
310
+ end
311
+
312
+ def build_headers
313
+ headers = @headers
314
+ if @content_type_header.present? && headers.keys.none? { |key| /^content[\-_]type$/i =~ key }
315
+ headers[:content_type] = @content_type_header
316
+ end
317
+ if @accept_header && headers.keys.none? { |key| /^accept$/i =~ key }
318
+ headers[:accept] = @accept_header
319
+ end
320
+ headers.compact
321
+ end
322
+
323
+ def detect_error!(response)
324
+ error_patterns = Array.wrap(@authorization[:detect_on])
325
+ return unless error_patterns.any? { |pattern| pattern === response rescue false }
326
+
327
+ Kernel.raise(CustomRequestError, response.to_s)
328
+ end
329
+
330
+ def after_error_response_matches?(exception)
331
+ return if @after_error_response_matches.blank?
332
+
333
+ @after_error_response_matches.find do |match|
334
+ case match
335
+ when ::Integer
336
+ match == exception.http_code
337
+ when ::String
338
+ exception.message.to_s.match(match) || exception.http_body&.match(match)
339
+ when ::Regexp
340
+ match =~ exception.message || match =~ exception.http_body
341
+ end
342
+ end
343
+ end
344
+
345
+ def apply_after_error_response(exception)
346
+ within_action_context(
347
+ exception.http_code,
348
+ exception.http_body,
349
+ exception.http_headers&.with_indifferent_access || {},
350
+ exception.message,
351
+ &@after_error_response
352
+ )
353
+ end
354
+
355
+ def within_action_context(*args, &block)
356
+ (@action || self).instance_exec(*args, &block)
357
+ end
358
+
359
+ def authorized
360
+ apply = @authorization[:apply] || @authorization[:credentials]
361
+ return yield unless apply
362
+
363
+ first = true
364
+ begin
365
+ settings = @settings.with_indifferent_access
366
+ if /oauth2/i =~ @authorization[:type]
367
+ instance_exec(settings, settings[:access_token], @auth_type, &apply)
368
+ else
369
+ instance_exec(settings, @auth_type, &apply)
370
+ end
371
+ yield
372
+ rescue StandardError => e
373
+ Kernel.raise e unless first
374
+ Kernel.raise e unless @action&.refresh_authorization!(
375
+ e.try(:http_code),
376
+ e.try(:http_body),
377
+ e.message,
378
+ @settings
379
+ )
380
+
381
+ first = false
382
+ retry
383
+ end
384
+ end
385
+
386
+ class Part < StringIO
387
+ def initialize(path, content_type, original_filename, *args)
388
+ super(*args)
389
+ @path = path
390
+ @content_type = content_type
391
+ @original_filename = original_filename
392
+ end
393
+
394
+ attr_reader :path, :content_type, :original_filename
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/encrypted_configuration'
4
+
5
+ module Workato
6
+ module Connector
7
+ module Sdk
8
+ class Settings
9
+ class << self
10
+ def from_file(path = DEFAULT_SETTINGS_PATH, name = nil)
11
+ new(path: path, name: name, encrypted: false).read
12
+ end
13
+
14
+ def from_encrypted_file(path = DEFAULT_ENCRYPTED_SETTINGS_PATH, key_path = nil, name = nil)
15
+ new(path: path, name: name, key_path: key_path, encrypted: true).read
16
+ end
17
+
18
+ def from_default_file(name = nil)
19
+ new(name: name).read
20
+ end
21
+ end
22
+
23
+ def initialize(path: nil, encrypted: nil, name: nil, key_path: nil)
24
+ @path = path
25
+ @name = name
26
+ @key_path = key_path
27
+ @encrypted = encrypted
28
+ end
29
+
30
+ def read
31
+ if path.nil?
32
+ if File.exist?(DEFAULT_ENCRYPTED_SETTINGS_PATH)
33
+ @encrypted = true
34
+ @path = DEFAULT_ENCRYPTED_SETTINGS_PATH
35
+ read_encrypted_file
36
+ elsif File.exist?(DEFAULT_SETTINGS_PATH)
37
+ @encrypted = false
38
+ @path = DEFAULT_SETTINGS_PATH
39
+ read_plain_file
40
+ else
41
+ @encrypted = false
42
+ {}
43
+ end
44
+ elsif encrypted.nil?
45
+ begin
46
+ @encrypted = false
47
+ read_plain_file
48
+ rescue StandardError
49
+ @encrypted = true
50
+ read_encrypted_file
51
+ end
52
+ elsif encrypted
53
+ read_encrypted_file
54
+ else
55
+ read_plain_file
56
+ end
57
+ end
58
+
59
+ def update(new_settings)
60
+ if encrypted
61
+ update_encrypted_file(new_settings)
62
+ else
63
+ update_plain_file(new_settings)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :key_path,
70
+ :name,
71
+ :path,
72
+ :encrypted
73
+
74
+ def read_plain_file
75
+ all_settings = File.open(path) do |f|
76
+ YAML.safe_load(f.read, [::Symbol]).to_hash.with_indifferent_access
77
+ end
78
+
79
+ name ? all_settings.fetch(name) : all_settings
80
+ end
81
+
82
+ def update_plain_file(new_settings)
83
+ @path ||= DEFAULT_SETTINGS_PATH
84
+ File.write(path, YAML.dump({})) unless File.exist?(path)
85
+
86
+ all_settings = self.class.from_file(path)
87
+
88
+ merge_settings(all_settings, new_settings)
89
+
90
+ File.write(path, serialize(all_settings))
91
+ end
92
+
93
+ def read_encrypted_file
94
+ all_settings = encrypted_configuration.config.with_indifferent_access
95
+
96
+ name ? all_settings.fetch(name) : all_settings
97
+ end
98
+
99
+ def update_encrypted_file(new_settings)
100
+ all_settings = encrypted_configuration.config.with_indifferent_access
101
+
102
+ merge_settings(all_settings, new_settings)
103
+
104
+ encrypted_configuration.write(serialize(all_settings))
105
+ end
106
+
107
+ def merge_settings(all_settings, new_settings)
108
+ if name
109
+ all_settings[name] = new_settings
110
+ else
111
+ all_settings.merge!(new_settings)
112
+ end
113
+ end
114
+
115
+ def encrypted_configuration
116
+ @encrypted_configuration ||= ActiveSupport::EncryptedConfiguration.new(
117
+ config_path: path,
118
+ key_path: key_path || DEFAULT_MASTER_KEY_PATH,
119
+ env_key: DEFAULT_MASTER_KEY_ENV,
120
+ raise_if_missing_key: true
121
+ )
122
+ end
123
+
124
+ def serialize(settings)
125
+ YAML.dump(settings.to_hash)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end