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