workato-connector-sdk 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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)|