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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module Connector
5
+ module Sdk
6
+ module Dsl
7
+ module LookupTable
8
+ def lookup(lookup_table_id, *args)
9
+ LookupTables.lookup(lookup_table_id, *args)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module Connector
5
+ module Sdk
6
+ module Dsl
7
+ module Time
8
+ def now
9
+ ::Time.zone.now
10
+ end
11
+
12
+ def today
13
+ ::Time.zone.today
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ ::Time.zone = Workato::Connector::Sdk::DEFAULT_TIME_ZONE
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module Workato
6
+ module Connector
7
+ module Sdk
8
+ module Dsl
9
+ module WorkatoCodeLib
10
+ JWT_ALGORITHMS = %w[RS256 RS384 RS512].freeze
11
+ JWT_RSA_KEY_MIN_LENGTH = 2048
12
+
13
+ def workato
14
+ WorkatoCodeLib
15
+ end
16
+
17
+ def parse_json(source)
18
+ WorkatoCodeLib.parse_json(source)
19
+ end
20
+
21
+ class << self
22
+ def jwt_encode_rs256(payload, key, header_fields = {})
23
+ jwt_encode(payload, key, 'RS256', header_fields)
24
+ end
25
+
26
+ def jwt_encode(payload, key, algorithm, header_fields = {})
27
+ algorithm = algorithm.to_s.upcase
28
+ unless JWT_ALGORITHMS.include?(algorithm)
29
+ raise "Unsupported signing method. Supports only #{JWT_ALGORITHMS.join(', ')}. Got: '#{algorithm}'"
30
+ end
31
+
32
+ rsa_private = OpenSSL::PKey::RSA.new(key)
33
+ if rsa_private.n.num_bits < JWT_RSA_KEY_MIN_LENGTH
34
+ raise "A RSA key of size #{JWT_RSA_KEY_MIN_LENGTH} bits or larger MUST be used with JWT."
35
+ end
36
+
37
+ header_fields = header_fields.present? ? header_fields.with_indifferent_access.except(:typ, :alg) : {}
38
+ ::JWT.encode(payload, rsa_private, algorithm, header_fields)
39
+ end
40
+
41
+ def parse_yaml(yaml)
42
+ ::Psych.safe_load(yaml)
43
+ rescue ::Psych::DisallowedClass => e
44
+ raise e.message
45
+ end
46
+
47
+ def render_yaml(obj)
48
+ ::Psych.dump(obj)
49
+ end
50
+
51
+ def parse_json(source)
52
+ JSON.parse(source)
53
+ end
54
+
55
+ def uuid
56
+ SecureRandom.uuid
57
+ end
58
+
59
+ RANDOM_SIZE = 32
60
+
61
+ def random_bytes(len)
62
+ unless (len.is_a? ::Integer) && (len <= RANDOM_SIZE)
63
+ raise "The requested length or random bytes sequence should be <= #{RANDOM_SIZE}"
64
+ end
65
+
66
+ String::Binary.new(::OpenSSL::Random.random_bytes(len))
67
+ end
68
+
69
+ ALLOWED_KEY_SIZES = [128, 192, 256].freeze
70
+
71
+ def aes_cbc_encrypt(string, key, init_vector = nil)
72
+ key_size = key.bytesize * 8
73
+ unless ALLOWED_KEY_SIZES.include?(key_size)
74
+ raise 'Incorrect key size for AES'
75
+ end
76
+
77
+ cipher = ::OpenSSL::Cipher.new("AES-#{key_size}-CBC")
78
+ cipher.encrypt
79
+ cipher.key = key
80
+ cipher.iv = init_vector if init_vector.present?
81
+ String::Binary.new(cipher.update(string) + cipher.final)
82
+ end
83
+
84
+ def aes_cbc_decrypt(string, key, init_vector = nil)
85
+ key_size = key.bytesize * 8
86
+ unless ALLOWED_KEY_SIZES.include?(key_size)
87
+ raise 'Incorrect key size for AES'
88
+ end
89
+
90
+ cipher = ::OpenSSL::Cipher.new("AES-#{key_size}-CBC")
91
+ cipher.decrypt
92
+ cipher.key = key
93
+ cipher.iv = init_vector if init_vector.present?
94
+ String::Binary.new(cipher.update(string) + cipher.final)
95
+ end
96
+
97
+ def pbkdf2_hmac_sha1(string, salt, iterations = 1000, key_len = 16)
98
+ String::Binary.new(::OpenSSL::PKCS5.pbkdf2_hmac_sha1(string, salt, iterations, key_len))
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module Connector
5
+ module Sdk
6
+ module Dsl
7
+ module WorkatoSchema
8
+ def workato_schema(id)
9
+ WorkatoSchemas.find(id)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './block_invocation_refinements'
4
+
5
+ require_relative './dsl/http'
6
+ require_relative './dsl/call'
7
+ require_relative './dsl/error'
8
+ require_relative './dsl/account_property'
9
+ require_relative './dsl/lookup_table'
10
+ require_relative './dsl/workato_code_lib'
11
+ require_relative './dsl/workato_schema'
12
+ require_relative './dsl/time'
13
+
14
+ module Workato
15
+ module Connector
16
+ module Sdk
17
+ module Dsl
18
+ module Global
19
+ include Time
20
+ include AccountProperty
21
+ include LookupTable
22
+ include WorkatoCodeLib
23
+ include WorkatoSchema
24
+
25
+ def sleep(seconds)
26
+ ::Kernel.sleep(seconds.presence || 0)
27
+ end
28
+ end
29
+
30
+ class WithDsl
31
+ include Global
32
+
33
+ using BlockInvocationRefinements
34
+
35
+ def execute(*args, &block)
36
+ instance_exec(*args, &block)
37
+ end
38
+
39
+ def self.execute(*args, &block)
40
+ WithDsl.new.execute(*args, &block)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module Connector
5
+ module Sdk
6
+ InvalidDefinitionError = Class.new(StandardError)
7
+
8
+ CustomRequestError = Class.new(StandardError)
9
+
10
+ class RequestError < StandardError
11
+ attr_reader :method,
12
+ :code,
13
+ :response
14
+
15
+ def initialize(message:, method:, code:, response:)
16
+ super(message)
17
+ @method = method
18
+ @code = code
19
+ @response = response
20
+ end
21
+ end
22
+
23
+ class NotImplementedError < RuntimeError
24
+ def initialize(msg = 'This part of Connector SDK is not implemented in workato-connector-sdk yet')
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+ require 'singleton'
5
+
6
+ module Workato
7
+ module Connector
8
+ module Sdk
9
+ class LookupTables
10
+ include Singleton
11
+
12
+ def self.from_yaml(path = DEFAULT_LOOKUP_TABLES_PATH)
13
+ instance.load_data(YAML.load_file(path))
14
+ end
15
+
16
+ def self.from_csv(table_id, table_name, path)
17
+ rows = CSV.foreach(path, headers: true, return_headers: false).map(&:to_h)
18
+ instance.load_data(table_name => { id: table_id, rows: rows })
19
+ end
20
+
21
+ class << self
22
+ delegate :load_data,
23
+ :lookup,
24
+ to: :instance
25
+ end
26
+
27
+ def lookup(table_name_or_id, *args)
28
+ table = find_table(table_name_or_id)
29
+ return {} unless table
30
+
31
+ condition = args.extract_options!
32
+ row = table.lazy.where(condition).first
33
+ return {} unless row
34
+
35
+ row.to_hash.with_indifferent_access
36
+ end
37
+
38
+ def load_data(data = {})
39
+ @table_by_id ||= {}
40
+ @table_by_name ||= {}
41
+ data.each do |name, table|
42
+ table = table.with_indifferent_access
43
+ rows = table['rows'].freeze
44
+ @table_by_id[table['id'].to_i] = rows
45
+ @table_by_name[name] = rows
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def find_table(table_name_or_id)
52
+ unless @table_by_id
53
+ raise 'Lookup Tables are not initialized. ' \
54
+ 'Init data by calling LookupTable.from_file or LookupTable.load_data'
55
+ end
56
+
57
+ (table_name_or_id.is_int? && @table_by_id[table_name_or_id.to_i]) || @table_by_name[table_name_or_id]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './block_invocation_refinements'
4
+
5
+ module Workato
6
+ module Connector
7
+ module Sdk
8
+ class ObjectDefinitions
9
+ using BlockInvocationRefinements
10
+
11
+ def initialize(object_definitions:, connection:, methods:, settings:)
12
+ @object_definitions_source = object_definitions
13
+ @methods = methods
14
+ @connection = connection
15
+ @settings = settings
16
+ define_object_definition_methods(object_definitions)
17
+ end
18
+
19
+ def lazy(settings = nil, config_fields = {})
20
+ object_definitions_lazy_hash = DupHashWithIndifferentAccess.new do |h, name|
21
+ fields_proc = @object_definitions_source[name][:fields]
22
+ h[name] = Action.new(
23
+ action: {
24
+ execute: lambda do |connection, input|
25
+ instance_exec(connection, input, object_definitions_lazy_hash, &fields_proc)
26
+ end
27
+ },
28
+ methods: @methods,
29
+ connection: @connection,
30
+ settings: @settings
31
+ ).execute(settings, config_fields)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :methods,
38
+ :connection,
39
+ :objects,
40
+ :settings
41
+
42
+ def define_object_definition_methods(object_definitions)
43
+ object_definitions.each do |(object, _definition)|
44
+ define_singleton_method(object) do
45
+ @object_definitions ||= {}
46
+ @object_definitions[object] ||= ObjectDefinition.new(name: object, object_definitions: self)
47
+ end
48
+ end
49
+ end
50
+
51
+ class ObjectDefinition
52
+ def initialize(name:, object_definitions:)
53
+ @object_definitions = object_definitions
54
+ @name = name
55
+ end
56
+
57
+ def fields(settings = nil, config_fields = {})
58
+ object_definitions_lazy_hash = @object_definitions.lazy(settings, config_fields)
59
+ object_definitions_lazy_hash[@name]
60
+ end
61
+ end
62
+
63
+ class DupHashWithIndifferentAccess < HashWithIndifferentAccess
64
+ def [](name)
65
+ super.deep_dup
66
+ end
67
+ end
68
+
69
+ private_constant 'ObjectDefinition'
70
+ private_constant 'DupHashWithIndifferentAccess'
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './dsl'
4
+ require_relative './block_invocation_refinements'
5
+
6
+ module Workato
7
+ module Connector
8
+ module Sdk
9
+ class Operation
10
+ include Dsl::Global
11
+ include Dsl::HTTP
12
+ include Dsl::Call
13
+ include Dsl::Error
14
+
15
+ using BlockInvocationRefinements
16
+
17
+ cattr_accessor :on_settings_updated
18
+
19
+ def initialize(operation:, connection: {}, methods: {}, settings: {}, object_definitions: nil)
20
+ @settings = settings.with_indifferent_access
21
+ @operation = operation.with_indifferent_access
22
+ @connection = connection.with_indifferent_access
23
+ @_methods = methods.with_indifferent_access
24
+ @object_definitions = object_definitions
25
+ end
26
+
27
+ def execute(settings = nil, input = {}, extended_input_schema = [], extended_output_schema = [], &block)
28
+ @settings = settings.with_indifferent_access if settings # is being used in request for refresh tokens
29
+ request_or_result = instance_exec(
30
+ @settings.with_indifferent_access, # a copy of settings hash is being used in executable blocks
31
+ input.with_indifferent_access,
32
+ Array.wrap(extended_input_schema).map(&:with_indifferent_access),
33
+ Array.wrap(extended_output_schema).map(&:with_indifferent_access),
34
+ &block
35
+ )
36
+ resolve_request(request_or_result)
37
+ end
38
+
39
+ def extended_schema(settings = nil, config_fields = {})
40
+ object_definitions_hash = object_definitions.lazy(settings, config_fields)
41
+ {
42
+ input: schema_fields(object_definitions_hash, settings, config_fields, &operation[:input_fields]),
43
+ output: schema_fields(object_definitions_hash, settings, config_fields, &operation[:output_fields])
44
+ }.with_indifferent_access
45
+ end
46
+
47
+ def input_fields(settings = nil, config_fields = {})
48
+ object_definitions_hash = object_definitions.lazy(settings, config_fields)
49
+ schema_fields(object_definitions_hash, settings, config_fields, &operation[:input_fields])
50
+ end
51
+
52
+ def output_fields(settings = nil, config_fields = {})
53
+ object_definitions_hash = object_definitions.lazy(settings, config_fields)
54
+ schema_fields(object_definitions_hash, settings, config_fields, &operation[:output_fields])
55
+ end
56
+
57
+ def summarize_input(input = {})
58
+ summarize(input, operation[:summarize_input])
59
+ end
60
+
61
+ def summarize_output(output = {})
62
+ summarize(output, operation[:summarize_output])
63
+ end
64
+
65
+ def sample_output(settings = nil, input = {})
66
+ execute(settings, input, &operation[:sample_output])
67
+ end
68
+
69
+ def refresh_authorization!(http_code, http_body, exception, settings = {})
70
+ return unless refresh_auth?(http_code, http_body, exception)
71
+
72
+ new_settings = if /oauth2/i =~ connection[:authorization][:type]
73
+ refresh_oauth2_token(settings)
74
+ elsif connection[:authorization][:acquire]
75
+ acquire_token(settings)
76
+ end
77
+ return unless new_settings
78
+
79
+ settings.merge!(new_settings)
80
+
81
+ on_settings_updated&.call(http_body, http_code, exception, settings)
82
+
83
+ settings
84
+ end
85
+
86
+ private
87
+
88
+ def summarize(data, paths)
89
+ return data unless paths.present?
90
+
91
+ Summarize.new(data: data, paths: paths).call
92
+ end
93
+
94
+ def schema_fields(object_definitions_hash, settings, config_fields, &schema_proc)
95
+ return {} unless schema_proc
96
+
97
+ execute(settings, config_fields) do |connection, input|
98
+ instance_exec(
99
+ object_definitions_hash,
100
+ connection,
101
+ input,
102
+ &schema_proc
103
+ )
104
+ end
105
+ end
106
+
107
+ def resolve_request(request_or_result)
108
+ case request_or_result
109
+ when Request
110
+ resolve_request(request_or_result.execute!)
111
+ when ::Array
112
+ request_or_result.each_with_index.inject(request_or_result) do |acc, (item, index)|
113
+ response_item = resolve_request(item)
114
+ if response_item.equal?(item)
115
+ acc
116
+ else
117
+ (acc == request_or_result ? acc.dup : acc).tap { |a| a[index] = response_item }
118
+ end
119
+ end
120
+ when ::Hash
121
+ request_or_result.inject(request_or_result.with_indifferent_access) do |acc, (key, value)|
122
+ response_value = resolve_request(value)
123
+ if response_value.equal?(value)
124
+ acc
125
+ else
126
+ (acc == request_or_result ? acc.dup : acc).tap { |h| h[key] = response_value }
127
+ end
128
+ end
129
+ else
130
+ request_or_result
131
+ end
132
+ end
133
+
134
+ def refresh_auth?(http_code, http_body, exception)
135
+ refresh_on = Array.wrap(connection[:authorization][:refresh_on]).compact
136
+ refresh_on.blank? || refresh_on.any? do |pattern|
137
+ pattern.is_a?(::Integer) && pattern == http_code ||
138
+ pattern === exception&.to_s ||
139
+ pattern === http_body
140
+ end
141
+ end
142
+
143
+ def acquire_token(settings)
144
+ acquire = connection[:authorization][:acquire]
145
+ raise InvalidDefinitionError, "'acquire' block is required for authorization" unless acquire
146
+
147
+ Action.new(
148
+ action: {
149
+ execute: ->(connection) { instance_exec(connection, &acquire) }
150
+ },
151
+ connection: connection.merge(
152
+ authorization: connection[:authorization].merge(
153
+ apply: nil
154
+ )
155
+ ),
156
+ methods: @_methods
157
+ ).execute(settings)
158
+ end
159
+
160
+ def refresh_oauth2_token_using_refresh(settings)
161
+ refresh = connection[:authorization][:refresh]
162
+ new_tokens, new_settings = Action.new(
163
+ action: {
164
+ execute: lambda do |connection|
165
+ instance_exec(connection, connection[:refresh_token], &refresh)
166
+ end
167
+ },
168
+ methods: @_methods
169
+ ).execute(settings)
170
+
171
+ new_tokens.with_indifferent_access.merge(new_settings || {})
172
+ end
173
+
174
+ def refresh_oauth2_token_using_token_url(settings)
175
+ if settings[:refresh_token].blank?
176
+ raise NotImplementedError, 'workato-connector-sdk does not support OAuth2 authorization process. '\
177
+ 'Use Workato Debugger UI to acquire access_token and refresh_token'
178
+ end
179
+
180
+ response = RestClient::Request.execute(
181
+ url: connection[:authorization][:token_url].call(settings),
182
+ method: :post,
183
+ payload: {
184
+ client_id: connection[:authorization][:client_id].call(settings),
185
+ client_secret: connection[:authorization][:client_secret].call(settings),
186
+ grant_type: :refresh_token,
187
+ refresh_token: settings[:refresh_token]
188
+ },
189
+ headers: {
190
+ accept: :json
191
+ }
192
+ )
193
+ tokens = JSON.parse(response.body)
194
+ {
195
+ access_token: tokens['access_token'],
196
+ refresh_token: tokens['refresh_token'].presence || settings[:refresh_token]
197
+ }.with_indifferent_access
198
+ end
199
+
200
+ def refresh_oauth2_token(settings)
201
+ if connection[:authorization][:refresh]
202
+ refresh_oauth2_token_using_refresh(settings)
203
+ elsif connection[:authorization][:token_url]
204
+ refresh_oauth2_token_using_token_url(settings)
205
+ else
206
+ raise InvalidDefinitionError, "'refresh' block or 'token_url' is required for refreshing the token"
207
+ end
208
+ end
209
+
210
+ attr_reader :operation,
211
+ :connection,
212
+ :settings,
213
+ :object_definitions
214
+ end
215
+ end
216
+ end
217
+ end