workato-connector-sdk 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.
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