workato-connector-sdk 0.1.0 → 0.3.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +57 -30
  3. data/exe/workato +12 -1
  4. data/lib/workato/cli/edit_command.rb +1 -3
  5. data/lib/workato/cli/exec_command.rb +21 -5
  6. data/lib/workato/cli/generate_command.rb +1 -2
  7. data/lib/workato/cli/generators/connector_generator.rb +4 -1
  8. data/lib/workato/cli/main.rb +50 -3
  9. data/lib/workato/cli/oauth2_command.rb +180 -0
  10. data/lib/workato/cli/push_command.rb +36 -24
  11. data/lib/workato/connector/sdk/action.rb +52 -4
  12. data/lib/workato/connector/sdk/connection.rb +144 -0
  13. data/lib/workato/connector/sdk/connector.rb +24 -9
  14. data/lib/workato/connector/sdk/dsl/time.rb +8 -1
  15. data/lib/workato/connector/sdk/dsl/workato_code_lib.rb +4 -4
  16. data/lib/workato/connector/sdk/errors.rb +13 -0
  17. data/lib/workato/connector/sdk/object_definitions.rb +10 -10
  18. data/lib/workato/connector/sdk/operation.rb +37 -39
  19. data/lib/workato/connector/sdk/request.rb +11 -7
  20. data/lib/workato/connector/sdk/schema/field/array.rb +25 -0
  21. data/lib/workato/connector/sdk/schema/field/convertors.rb +189 -0
  22. data/lib/workato/connector/sdk/schema/field/date.rb +28 -0
  23. data/lib/workato/connector/sdk/schema/field/date_time.rb +28 -0
  24. data/lib/workato/connector/sdk/schema/field/integer.rb +27 -0
  25. data/lib/workato/connector/sdk/schema/field/number.rb +27 -0
  26. data/lib/workato/connector/sdk/schema/field/object.rb +25 -0
  27. data/lib/workato/connector/sdk/schema/field/string.rb +26 -0
  28. data/lib/workato/connector/sdk/schema/type/time.rb +53 -0
  29. data/lib/workato/connector/sdk/schema/type/unicode_string.rb +22 -0
  30. data/lib/workato/connector/sdk/schema.rb +230 -0
  31. data/lib/workato/connector/sdk/settings.rb +6 -3
  32. data/lib/workato/connector/sdk/trigger.rb +25 -0
  33. data/lib/workato/connector/sdk/version.rb +1 -1
  34. data/lib/workato/connector/sdk.rb +1 -0
  35. data/lib/workato/extension/string.rb +16 -10
  36. data/lib/workato/web/app.rb +23 -0
  37. data/templates/Gemfile.erb +1 -0
  38. data/templates/spec/action_spec.rb.erb +7 -1
  39. data/templates/spec/trigger_spec.rb.erb +6 -0
  40. metadata +95 -7
@@ -3,6 +3,7 @@
3
3
  require 'uri'
4
4
  require 'ruby-progressbar'
5
5
  require 'zip'
6
+ require 'fileutils'
6
7
 
7
8
  module Workato
8
9
  module CLI
@@ -19,6 +20,7 @@ module Workato
19
20
  'live-eu' => 'https://app.eu.workato.com'
20
21
  }.freeze
21
22
 
23
+ API_USER_PATH = '/api/users/me'
22
24
  API_IMPORT_PATH = '/api/packages/import'
23
25
  API_PACKAGE_PATH = '/api/packages'
24
26
  IMPORT_IN_PROGRESS = 'in_progress'
@@ -30,38 +32,33 @@ module Workato
30
32
  AWAIT_IMPORT_SLEEP_INTERVAL = 15 # seconds
31
33
  AWAIT_IMPORT_TIMEOUT_INTERVAL = 120 # seconds
32
34
 
33
- def initialize(folder:, options:)
34
- @folder_id = folder
35
+ def initialize(options:)
35
36
  @options = options
36
37
  @api_base_url = ENVIRONMENTS.fetch(options[:environment])
37
38
  @api_email = options[:api_email] || ENV[WORKATO_API_EMAIL_ENV]
38
39
  @api_token = options[:api_token] || ENV[WORKATO_API_TOKEN_ENV]
40
+ @folder_id = options[:folder]
39
41
  end
40
42
 
41
43
  def call
42
- zip_file = build_package
44
+ zip_file_path = build_package
43
45
  say_status :success, 'Build package' if verbose?
44
46
 
45
- import_id = import_package(zip_file)
47
+ import_id = import_package(zip_file_path)
46
48
  say_status :success, 'Upload package' if verbose?
47
49
  say_status :waiting, 'Process package' if verbose?
48
50
 
49
51
  result = await_import(import_id)
50
- if result.fetch('status') == 'failed'
51
- say result.fetch('error').gsub("#{PACKAGE_ENTRY_NAME}.json: ", '')
52
- else
53
- say "Connector was successfully uploaded to #{api_base_url}"
54
- end
55
- rescue StandardError => e
56
- say e.message
52
+ raise human_friendly_error(result) if result.fetch('status') == 'failed'
53
+
54
+ say "Connector was successfully uploaded to #{api_base_url}"
57
55
  ensure
58
- zip_file&.close(true)
56
+ FileUtils.rm_f(zip_file_path) if zip_file_path
59
57
  end
60
58
 
61
59
  private
62
60
 
63
61
  attr_reader :options,
64
- :folder_id,
65
62
  :api_token,
66
63
  :api_email,
67
64
  :api_base_url
@@ -78,16 +75,13 @@ module Workato
78
75
  end
79
76
 
80
77
  def build_package
81
- zip_file = Tempfile.new(['connector', '.zip'])
82
-
83
- ::Zip::OutputStream.open(zip_file.path) { |_| 'no-op' }
84
- ::Zip::File.open(zip_file.path, ::Zip::File::CREATE) do |archive|
85
- add_connector(archive)
86
- add_manifest(archive)
87
- add_logo(archive)
78
+ ::Dir::Tmpname.create(['connector', '.zip']) do |path|
79
+ ::Zip::File.open(path, ::Zip::File::CREATE) do |archive|
80
+ add_connector(archive)
81
+ add_manifest(archive)
82
+ add_logo(archive)
83
+ end
88
84
  end
89
-
90
- zip_file
91
85
  end
92
86
 
93
87
  def add_connector(archive)
@@ -110,11 +104,11 @@ module Workato
110
104
  end
111
105
  end
112
106
 
113
- def import_package(zip_file)
107
+ def import_package(zip_file_path)
114
108
  url = "#{api_base_url}#{API_IMPORT_PATH}/#{folder_id}"
115
109
  response = RestClient.post(
116
110
  url,
117
- File.open(zip_file.path),
111
+ File.open(zip_file_path),
118
112
  auth_headers.merge(
119
113
  'Content-Type' => 'application/zip'
120
114
  )
@@ -197,7 +191,25 @@ module Workato
197
191
  }
198
192
  end
199
193
 
194
+ def folder_id
195
+ @folder_id ||=
196
+ begin
197
+ url = "#{api_base_url}#{API_USER_PATH}"
198
+ response = RestClient.get(url, auth_headers)
199
+
200
+ json = JSON.parse(response.body)
201
+ json.fetch('root_folder_id').tap do |folder_id|
202
+ say_status :success, "Fetch root folder ID: #{folder_id}" if verbose?
203
+ end
204
+ end
205
+ end
206
+
207
+ def human_friendly_error(result)
208
+ result.fetch('error').gsub("#{PACKAGE_ENTRY_NAME}.json: ", '')
209
+ end
210
+
200
211
  private_constant :IMPORT_IN_PROGRESS,
212
+ :API_USER_PATH,
201
213
  :API_IMPORT_PATH,
202
214
  :API_PACKAGE_PATH,
203
215
  :PACKAGE_ENTRY_NAME,
@@ -14,6 +14,8 @@ module Workato
14
14
  RETRY_DELAY = 5.seconds
15
15
  MAX_RETRIES = 3
16
16
 
17
+ MAX_REINVOKES = 5
18
+
17
19
  def initialize(action:, connection: {}, methods: {}, settings: {}, object_definitions: nil)
18
20
  super(
19
21
  operation: action,
@@ -26,10 +28,32 @@ module Workato
26
28
  initialize_retry
27
29
  end
28
30
 
29
- def execute(settings = nil, input = {}, extended_input_schema = [], extended_output_schema = [], &block)
31
+ def execute(settings = nil, input = {}, extended_input_schema = [], extended_output_schema = [], continue = {},
32
+ &block)
30
33
  raise InvalidDefinitionError, "'execute' block is required for action" unless block || action[:execute]
31
34
 
32
- super(settings, input, extended_input_schema, extended_output_schema, &(block || action[:execute]))
35
+ loop do
36
+ if @reinvokes_remaining&.zero?
37
+ raise "Max number of reinvokes on SDK Gem reached. Current limit is #{reinvoke_limit}"
38
+ end
39
+
40
+ reinvoke_sleep if @reinvoke_after
41
+
42
+ @reinvoke_after = nil
43
+
44
+ result = super(
45
+ settings,
46
+ input,
47
+ extended_input_schema,
48
+ extended_output_schema,
49
+ continue,
50
+ &(block || action[:execute])
51
+ )
52
+
53
+ break result unless @reinvoke_after
54
+
55
+ continue = @reinvoke_after[:continue]
56
+ end
33
57
  rescue RequestError => e
34
58
  raise e unless retry?(e)
35
59
 
@@ -37,12 +61,28 @@ module Workato
37
61
  sleep(RETRY_DELAY) && retry
38
62
  end
39
63
 
64
+ def invoke(input = {})
65
+ extended_schema = extended_schema(nil, input)
66
+ config_schema = Schema.new(schema: config_fields_schema)
67
+ input_schema = Schema.new(schema: extended_schema[:input])
68
+ output_schema = Schema.new(schema: extended_schema[:output])
69
+
70
+ input = apply_input_schema(input, config_schema + input_schema)
71
+ output = execute(nil, input, input_schema, output_schema)
72
+ apply_output_schema(output, output_schema)
73
+ end
74
+
40
75
  def checkpoint!(continue:, temp_output: nil)
41
- raise NotImplementedError
76
+ # no-op
42
77
  end
43
78
 
44
79
  def reinvoke_after(seconds:, continue:, temp_output: nil)
45
- raise NotImplementedError
80
+ @reinvokes_remaining = (@reinvokes_remaining ? @reinvokes_remaining - 1 : reinvoke_limit)
81
+ @reinvoke_after = {
82
+ seconds: seconds,
83
+ continue: continue,
84
+ temp_output: temp_output
85
+ }
46
86
  end
47
87
 
48
88
  private
@@ -81,6 +121,14 @@ module Workato
81
121
  @retry_methods.include?(exception.method.to_s.downcase)
82
122
  end
83
123
 
124
+ def reinvoke_sleep
125
+ sleep((ENV['WAIT_REINVOKE_AFTER'].presence || @reinvoke_after[:seconds]).to_f)
126
+ end
127
+
128
+ def reinvoke_limit
129
+ @reinvoke_limit ||= (ENV['MAX_REINVOKES'].presence || MAX_REINVOKES).to_i
130
+ end
131
+
84
132
  alias action operation
85
133
  end
86
134
  end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './block_invocation_refinements'
4
+
5
+ module Workato
6
+ module Connector
7
+ module Sdk
8
+ class Connection
9
+ using BlockInvocationRefinements
10
+
11
+ attr_reader :source
12
+
13
+ def initialize(connection: {}, methods: {}, settings: {})
14
+ @methods_source = methods.with_indifferent_access
15
+ @source = connection.with_indifferent_access
16
+ @settings = settings
17
+ end
18
+
19
+ def authorization
20
+ @authorization ||= Authorization.new(
21
+ connection: source,
22
+ methods: methods_source,
23
+ settings: @settings
24
+ )
25
+ end
26
+
27
+ def base_uri(settings = {})
28
+ source[:base_uri]&.call(settings.with_indifferent_access)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :methods_source
34
+
35
+ class Authorization
36
+ attr_reader :source
37
+
38
+ def initialize(connection: {}, methods: {}, settings: {})
39
+ @connection_source = connection.with_indifferent_access
40
+ @source = (connection[:authorization] || {}).with_indifferent_access
41
+ @methods_source = methods.with_indifferent_access
42
+ @settings = settings
43
+ end
44
+
45
+ def token_url?
46
+ source[:token_url].present?
47
+ end
48
+
49
+ def acquire?
50
+ source[:acquire].present?
51
+ end
52
+
53
+ def refresh?
54
+ source[:refresh].present?
55
+ end
56
+
57
+ def type
58
+ source[:type]
59
+ end
60
+
61
+ def refresh_on
62
+ Array.wrap(source[:refresh_on]).compact
63
+ end
64
+
65
+ def detect_on
66
+ Array.wrap(source[:detect_on]).compact
67
+ end
68
+
69
+ def client_id(settings = {})
70
+ client_id = source[:client_id]
71
+
72
+ if client_id.is_a?(Proc)
73
+ Dsl::WithDsl.execute(settings.with_indifferent_access, &client_id)
74
+ else
75
+ client_id
76
+ end
77
+ end
78
+
79
+ def client_secret(settings = {})
80
+ client_secret_source = source[:client_secret]
81
+
82
+ if client_secret_source.is_a?(Proc)
83
+ Dsl::WithDsl.execute(settings.with_indifferent_access, &client_secret_source)
84
+ else
85
+ client_secret_source
86
+ end
87
+ end
88
+
89
+ def authorization_url(settings = {})
90
+ source[:authorization_url]&.call(settings.with_indifferent_access)
91
+ end
92
+
93
+ def token_url(settings = {})
94
+ source[:token_url]&.call(settings.with_indifferent_access)
95
+ end
96
+
97
+ def acquire(settings = {}, oauth2_code = nil, redirect_url = nil)
98
+ acquire_proc = source[:acquire]
99
+ raise InvalidDefinitionError, "Expect 'acquire' block" unless acquire_proc
100
+
101
+ Workato::Connector::Sdk::Operation.new(
102
+ connection: Connection.new(
103
+ connection: connection_source.merge(
104
+ authorization: source.merge(
105
+ apply: nil
106
+ )
107
+ ),
108
+ methods: methods_source,
109
+ settings: @settings
110
+ ),
111
+ methods: methods_source,
112
+ settings: @settings
113
+ ).execute(settings, { auth_code: oauth2_code, redirect_url: redirect_url }) do |connection, input|
114
+ instance_exec(connection, input[:auth_code], input[:redirect_url], &acquire_proc)
115
+ end
116
+ end
117
+
118
+ def refresh(settings = {}, refresh_token = nil)
119
+ refresh_proc = source[:refresh]
120
+ raise InvalidDefinitionError, "Expect 'refresh' block" unless refresh_proc
121
+
122
+ Workato::Connector::Sdk::Operation.new(
123
+ connection: Connection.new(
124
+ methods: methods_source,
125
+ settings: @settings
126
+ ),
127
+ methods: methods_source,
128
+ settings: @settings
129
+ ).execute(settings, { refresh_token: refresh_token }) do |connection, input|
130
+ instance_exec(connection, input[:refresh_token], &refresh_proc)
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ attr_reader :connection_source,
137
+ :methods_source
138
+ end
139
+
140
+ private_constant :Authorization
141
+ end
142
+ end
143
+ end
144
+ end
@@ -24,9 +24,9 @@ module Workato
24
24
  def actions
25
25
  @actions ||= ActionsProxy.new(
26
26
  actions: source[:actions].presence || {},
27
- object_definitions: object_definitions,
28
27
  methods: methods_source,
29
- connection: connection_source,
28
+ object_definitions: object_definitions,
29
+ connection: connection,
30
30
  settings: settings
31
31
  )
32
32
  end
@@ -34,7 +34,7 @@ module Workato
34
34
  def methods
35
35
  @methods ||= MethodsProxy.new(
36
36
  methods: methods_source,
37
- connection: connection_source,
37
+ connection: connection,
38
38
  settings: settings
39
39
  )
40
40
  end
@@ -45,7 +45,7 @@ module Workato
45
45
  execute: source[:test]
46
46
  },
47
47
  methods: methods_source,
48
- connection: connection_source,
48
+ connection: connection,
49
49
  settings: send(:settings)
50
50
  ).execute(settings)
51
51
  end
@@ -53,9 +53,9 @@ module Workato
53
53
  def triggers
54
54
  @triggers ||= TriggersProxy.new(
55
55
  triggers: source[:triggers].presence || {},
56
- object_definitions: object_definitions,
57
56
  methods: methods_source,
58
- connection: connection_source,
57
+ connection: connection,
58
+ object_definitions: object_definitions,
59
59
  settings: settings
60
60
  )
61
61
  end
@@ -64,7 +64,7 @@ module Workato
64
64
  @object_definitions ||= ObjectDefinitions.new(
65
65
  object_definitions: source[:object_definitions].presence || {},
66
66
  methods: methods_source,
67
- connection: connection_source,
67
+ connection: connection,
68
68
  settings: settings
69
69
  )
70
70
  end
@@ -72,6 +72,14 @@ module Workato
72
72
  def pick_lists
73
73
  @pick_lists ||= PickListsProxy.new(
74
74
  pick_lists: source[:pick_lists].presence || {},
75
+ methods: methods_source,
76
+ connection: connection,
77
+ settings: settings
78
+ )
79
+ end
80
+
81
+ def connection
82
+ @connection ||= Connection.new(
75
83
  methods: methods_source,
76
84
  connection: connection_source,
77
85
  settings: settings
@@ -107,7 +115,7 @@ module Workato
107
115
 
108
116
  def define_action_methods(actions)
109
117
  actions.each do |action, definition|
110
- define_singleton_method(action) do
118
+ define_singleton_method(action) do |input_ = nil|
111
119
  @actions ||= {}
112
120
  @actions[action] ||= Action.new(
113
121
  action: definition,
@@ -116,6 +124,9 @@ module Workato
116
124
  connection: connection,
117
125
  settings: settings
118
126
  )
127
+ return @actions[action] if input_.nil?
128
+
129
+ @actions[action].invoke(input_)
119
130
  end
120
131
  end
121
132
  end
@@ -213,7 +224,7 @@ module Workato
213
224
 
214
225
  def define_trigger_methods(triggers)
215
226
  triggers.each do |trigger, definition|
216
- define_singleton_method(trigger) do
227
+ define_singleton_method(trigger) do |input_ = nil, payload = {}, headers = {}, params = {}|
217
228
  @triggers[trigger] ||= Trigger.new(
218
229
  trigger: definition,
219
230
  object_definitions: object_definitions,
@@ -221,6 +232,10 @@ module Workato
221
232
  connection: connection,
222
233
  settings: settings
223
234
  )
235
+
236
+ return @triggers[trigger] if input_.nil?
237
+
238
+ @triggers[trigger].invoke(input_, payload, headers, params)
224
239
  end
225
240
  end
226
241
  end
@@ -18,4 +18,11 @@ module Workato
18
18
  end
19
19
  end
20
20
 
21
- ::Time.zone = Workato::Connector::Sdk::DEFAULT_TIME_ZONE
21
+ begin
22
+ ::Time.zone = Workato::Connector::Sdk::DEFAULT_TIME_ZONE
23
+ rescue TZInfo::DataSourceNotFound
24
+ puts ''
25
+ puts "tzinfo-data is not present. Please install gem 'tzinfo-data' by 'gem install tzinfo-data'"
26
+ puts ''
27
+ exit!
28
+ end
@@ -63,7 +63,7 @@ module Workato
63
63
  raise "The requested length or random bytes sequence should be <= #{RANDOM_SIZE}"
64
64
  end
65
65
 
66
- String::Binary.new(::OpenSSL::Random.random_bytes(len))
66
+ Extension::Binary.new(::OpenSSL::Random.random_bytes(len))
67
67
  end
68
68
 
69
69
  ALLOWED_KEY_SIZES = [128, 192, 256].freeze
@@ -78,7 +78,7 @@ module Workato
78
78
  cipher.encrypt
79
79
  cipher.key = key
80
80
  cipher.iv = init_vector if init_vector.present?
81
- String::Binary.new(cipher.update(string) + cipher.final)
81
+ Extension::Binary.new(cipher.update(string) + cipher.final)
82
82
  end
83
83
 
84
84
  def aes_cbc_decrypt(string, key, init_vector = nil)
@@ -91,11 +91,11 @@ module Workato
91
91
  cipher.decrypt
92
92
  cipher.key = key
93
93
  cipher.iv = init_vector if init_vector.present?
94
- String::Binary.new(cipher.update(string) + cipher.final)
94
+ Extension::Binary.new(cipher.update(string) + cipher.final)
95
95
  end
96
96
 
97
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))
98
+ Extension::Binary.new(::OpenSSL::PKCS5.pbkdf2_hmac_sha1(string, salt, iterations, key_len))
99
99
  end
100
100
  end
101
101
  end
@@ -5,6 +5,8 @@ module Workato
5
5
  module Sdk
6
6
  InvalidDefinitionError = Class.new(StandardError)
7
7
 
8
+ InvalidSchemaError = Class.new(StandardError)
9
+
8
10
  CustomRequestError = Class.new(StandardError)
9
11
 
10
12
  class RequestError < StandardError
@@ -25,6 +27,17 @@ module Workato
25
27
  super
26
28
  end
27
29
  end
30
+
31
+ class MissingRequiredInput < StandardError
32
+ def initialize(label, toggle_label)
33
+ message = if toggle_label && label != toggle_label
34
+ "Either '#{label}' or '#{toggle_label}' must be present"
35
+ else
36
+ "'#{label}' must be present"
37
+ end
38
+ super(message)
39
+ end
40
+ end
28
41
  end
29
42
  end
30
43
  end
@@ -10,23 +10,23 @@ module Workato
10
10
 
11
11
  def initialize(object_definitions:, connection:, methods:, settings:)
12
12
  @object_definitions_source = object_definitions
13
- @methods = methods
13
+ @methods_source = methods
14
14
  @connection = connection
15
15
  @settings = settings
16
16
  define_object_definition_methods(object_definitions)
17
17
  end
18
18
 
19
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(
20
+ DupHashWithIndifferentAccess.new do |object_definitions, name|
21
+ fields_proc = object_definitions_source[name][:fields]
22
+ object_definitions[name] = Action.new(
23
23
  action: {
24
24
  execute: lambda do |connection, input|
25
- instance_exec(connection, input, object_definitions_lazy_hash, &fields_proc)
25
+ instance_exec(connection, input, object_definitions, &fields_proc)
26
26
  end
27
27
  },
28
- methods: @methods,
29
- connection: @connection,
28
+ methods: methods_source,
29
+ connection: connection,
30
30
  settings: @settings
31
31
  ).execute(settings, config_fields)
32
32
  end
@@ -34,10 +34,10 @@ module Workato
34
34
 
35
35
  private
36
36
 
37
- attr_reader :methods,
37
+ attr_reader :methods_source,
38
38
  :connection,
39
- :objects,
40
- :settings
39
+ :settings,
40
+ :object_definitions_source
41
41
 
42
42
  def define_object_definition_methods(object_definitions)
43
43
  object_definitions.each do |(object, _definition)|