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
data/exe/workato ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'workato/cli/main'
5
+ Workato::CLI::Main.start
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/encrypted_configuration'
4
+
5
+ module Workato
6
+ module CLI
7
+ class EditCommand
8
+ def initialize(path:, options:)
9
+ @encrypted_file_path = path
10
+ @key_path = options[:key] || Workato::Connector::Sdk::DEFAULT_MASTER_KEY_PATH
11
+ @encrypted_config ||= ActiveSupport::EncryptedConfiguration.new(
12
+ config_path: encrypted_file_path,
13
+ key_path: @key_path,
14
+ env_key: Workato::Connector::Sdk::DEFAULT_MASTER_KEY_ENV,
15
+ raise_if_missing_key: false
16
+ )
17
+ end
18
+
19
+ def call
20
+ ensure_editor_available || return
21
+ ensure_encryption_key_present
22
+
23
+ catch_editing_exceptions do
24
+ encrypted_config.change do |tmp_path|
25
+ system("#{ENV['EDITOR']} #{tmp_path}")
26
+ end
27
+ end
28
+
29
+ puts 'File encrypted and saved.'
30
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
31
+ puts "Couldn't decrypt #{encrypted_file_path}. Perhaps you passed the wrong key?"
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :key_path,
37
+ :encrypted_file_path,
38
+ :encrypted_config
39
+
40
+ def ensure_encryption_key_present
41
+ return if encrypted_config.key.present?
42
+
43
+ Generators::MasterKeyGenerator.new.call(@key_path)
44
+ end
45
+
46
+ def ensure_editor_available
47
+ return true if ENV['EDITOR'].to_s.present?
48
+
49
+ puts <<~HELP
50
+ No $EDITOR to open file in. Assign one like this:
51
+
52
+ EDITOR="mate --wait" workato edit #{encrypted_file_path}
53
+
54
+ For editors that fork and exit immediately, it's important to pass a wait flag,
55
+ otherwise the credentials will be saved immediately with no chance to edit.
56
+
57
+ HELP
58
+
59
+ false
60
+ end
61
+
62
+ def catch_editing_exceptions
63
+ yield
64
+ rescue Interrupt
65
+ puts 'Aborted changing file: nothing saved.'
66
+ rescue ActiveSupport::EncryptedFile::MissingKeyError => e
67
+ puts e.message
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'ruby-progressbar'
5
+
6
+ module Workato
7
+ module CLI
8
+ class ExecCommand
9
+ include Thor::Shell
10
+
11
+ def initialize(path:, options:)
12
+ @path = path
13
+ @options = options
14
+ end
15
+
16
+ def call
17
+ load_from_default_files
18
+ inspect_params(params)
19
+ output = with_progress { execute_path }
20
+ show_output(output)
21
+ output
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :path,
27
+ :options
28
+
29
+ # rubocop:disable Style/GuardClause
30
+ def load_from_default_files
31
+ if File.exist?(Workato::Connector::Sdk::DEFAULT_SCHEMAS_PATH)
32
+ Workato::Connector::Sdk::WorkatoSchemas.from_json
33
+ end
34
+ if File.exist?(Workato::Connector::Sdk::DEFAULT_LOOKUP_TABLES_PATH)
35
+ Workato::Connector::Sdk::LookupTables.from_yaml
36
+ end
37
+ if File.exist?(Workato::Connector::Sdk::DEFAULT_ACCOUNT_PROPERTIES_PATH)
38
+ Workato::Connector::Sdk::AccountProperties.from_yaml
39
+ end
40
+ if File.exist?(Workato::Connector::Sdk::DEFAULT_ENCRYPTED_ACCOUNT_PROPERTIES_PATH)
41
+ Workato::Connector::Sdk::AccountProperties.from_encrypted_yaml
42
+ end
43
+ end
44
+ # rubocop:enable Style/GuardClause
45
+
46
+ def params
47
+ @params ||= {
48
+ connector: connector,
49
+ settings: settings,
50
+ input: from_json(options[:input]),
51
+ output: from_json(options[:input]),
52
+ webhook_subscribe_output: from_json(options[:input]),
53
+ args: from_json(options[:args]).presence || [],
54
+ extended_input_schema: from_json(options[:extended_input_schema]).presence || [],
55
+ extended_output_schema: from_json(options[:extended_output_schema]).presence || [],
56
+ config_fields: from_json(options[:config_fields]),
57
+ closure: from_json(options[:closure], parse_json_times: true).presence,
58
+ payload: from_json(options[:webhook_payload]),
59
+ params: from_json(options[:webhook_params]),
60
+ headers: from_json(options[:webhook_headers]),
61
+ webhook_url: options[:webhook_url],
62
+ recipe_id: SecureRandom.uuid
63
+ }
64
+ end
65
+
66
+ def connector
67
+ Workato::Connector::Sdk::Connector.from_file(
68
+ options[:connector] || Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH,
69
+ settings
70
+ )
71
+ end
72
+
73
+ def settings
74
+ return @settings if defined?(@settings)
75
+
76
+ settings_store = Workato::Connector::Sdk::Settings.new(
77
+ path: options[:settings],
78
+ name: options[:connection],
79
+ key_path: options[:key]
80
+ )
81
+ @settings = settings_store.read
82
+
83
+ Workato::Connector::Sdk::Operation.on_settings_updated = lambda do |_, _, exception, new_settings|
84
+ $stdout.pause if verbose?
85
+ say('')
86
+ say("Refresh token triggered on response \"#{exception}\"")
87
+ loop do
88
+ answer = ask('Updated settings file with new connection attributes? (Yes or No)').to_s.downcase
89
+ break if %w[n no].include?(answer)
90
+ break settings_store.update(new_settings) if %w[y yes].include?(answer)
91
+ end
92
+ $stdout.resume if verbose?
93
+ end
94
+
95
+ @settings
96
+ end
97
+
98
+ def from_json(path, parse_json_times: false)
99
+ old_parse_json_times = ActiveSupport.parse_json_times
100
+ ::ActiveSupport.parse_json_times = parse_json_times
101
+ result = path ? ::ActiveSupport::JSON.decode(File.read(path)) : {}
102
+ ::ActiveSupport.parse_json_times = old_parse_json_times
103
+ result
104
+ end
105
+
106
+ def inspect_params(params)
107
+ return unless verbose?
108
+
109
+ if params[:settings].present?
110
+ say('SETTINGS')
111
+ jj params[:settings]
112
+ end
113
+
114
+ say('INPUT')
115
+ jj params[:input]
116
+ end
117
+
118
+ def execute_path
119
+ methods = path.split('.')
120
+ method = methods.pop
121
+ object = methods.inject(params[:connector]) { |obj, m| obj.public_send(m) }
122
+ parameters = object.method(method).parameters.reject { |p| p[0] == :block }.map(&:second)
123
+ args = params.values_at(*parameters)
124
+ if parameters.last == :args
125
+ args = args.take(args.length - 1) + Array.wrap(args.last).flatten(1)
126
+ end
127
+ object.public_send(method, *args)
128
+ rescue Exception => e # rubocop:disable Lint/RescueException
129
+ raise e if options[:debug]
130
+
131
+ e.message
132
+ end
133
+
134
+ def show_output(output)
135
+ if options[:output].present?
136
+ File.open(options[:output], 'w') do |f|
137
+ f.write(JSON.dump(output))
138
+ end
139
+ elsif options[:output].nil?
140
+ say('OUTPUT') if verbose?
141
+ jj output
142
+ end
143
+ end
144
+
145
+ def verbose?
146
+ !!options[:verbose]
147
+ end
148
+
149
+ def with_progress
150
+ return yield unless verbose?
151
+
152
+ say('')
153
+
154
+ old_stdout = $stdout
155
+ progress_bar = ProgressBar.create(total: nil, output: old_stdout)
156
+ $stdout = ProgressLogger.new(progress_bar)
157
+ RestClient.log = $stdout
158
+
159
+ dots = Thread.start do
160
+ loop do
161
+ progress_bar.increment unless progress_bar.paused?
162
+ sleep(0.2)
163
+ end
164
+ end
165
+ output = yield
166
+ dots.kill
167
+ progress_bar.finish
168
+
169
+ $stdout = old_stdout
170
+
171
+ say('')
172
+
173
+ output
174
+ end
175
+
176
+ class ProgressLogger < SimpleDelegator
177
+ def initialize(progress)
178
+ super($stdout)
179
+ @progress = progress
180
+ end
181
+
182
+ delegate :log, :pause, :resume, to: :@progress
183
+
184
+ alias << log
185
+ alias write log
186
+ alias puts log
187
+ alias print log
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module CLI
5
+ class GenerateCommand < Thor
6
+ include Thor::Actions
7
+
8
+ TEST_TYPES = %w[trigger action pick_list object_definition method].freeze
9
+
10
+ def self.source_root
11
+ File.expand_path('../../../templates', __dir__)
12
+ end
13
+
14
+ desc 'test', 'Generate empty test for connector'
15
+
16
+ method_option :connector, type: :string, aliases: '-c', desc: 'Path to connector source code',
17
+ lazy_default: Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH
18
+ method_option :action, type: :string, aliases: '-a', desc: 'Create test for Action with name'
19
+ method_option :trigger, type: :string, aliases: '-t', desc: 'Create test for Trigger with name'
20
+ method_option :pick_list, type: :string, aliases: '-p', desc: 'Create test for Pick List with name'
21
+ method_option :object_definition, type: :string, aliases: '-o',
22
+ desc: 'Create test for Object Definition with name'
23
+ method_option :method, type: :string, aliases: '-m', desc: 'Create test for Method with name'
24
+
25
+ long_desc <<~HELP
26
+ The 'workato generate test' command analyzes existing connector source and creates an empty test for
27
+ all connector's definitions.
28
+
29
+ Example:
30
+
31
+ workato generate test # This generates a skeletal tests for all connector's definitions.
32
+
33
+ workato generate test --action=test_action # This generates an empty test for `test_action` only.
34
+
35
+ workato generate test --method=some_method
36
+ HELP
37
+
38
+ def test
39
+ create_spec_files
40
+ end
41
+
42
+ desc 'schema', 'Generate schema by JSON example'
43
+ def schema
44
+ say <<~HELP
45
+ Generate schema is not implemented yet.
46
+ Use Workato UI "JSON to Schema" converter for now.
47
+ HELP
48
+ end
49
+
50
+ private
51
+
52
+ def create_spec_files
53
+ definitions = options.slice(*TEST_TYPES)
54
+ if definitions.any?
55
+ definitions.each do |type, name|
56
+ create_spec_file(type, name)
57
+ end
58
+ else
59
+ ensure_connector_source
60
+
61
+ template('spec/connector_spec.rb.erb', 'spec/connector_spec.rb', skip: true)
62
+ TEST_TYPES.each do |type|
63
+ (connector.source[type.pluralize] || {}).each do |(name, _definition)|
64
+ create_spec_file(type, name)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ def ensure_connector_source
71
+ return if connector
72
+
73
+ error "Can't find connector source code"
74
+ exit
75
+ end
76
+
77
+ def create_spec_file(type, name)
78
+ template(
79
+ partial(type),
80
+ "spec/#{type}s/#{sanitized_filename(name)}_spec.rb",
81
+ context: binding,
82
+ skip: true
83
+ )
84
+ end
85
+
86
+ def connector
87
+ return @connector if @connector
88
+
89
+ @connector = Workato::Connector::Sdk::Connector.from_file(
90
+ options[:connector] || Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH
91
+ )
92
+ rescue StandardError
93
+ nil
94
+ end
95
+
96
+ def partial(type)
97
+ "spec/#{type.downcase}_spec.rb.erb"
98
+ end
99
+
100
+ def sanitized_filename(name)
101
+ name.downcase.gsub(/[^0-9A-z.\-]/, '_')
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module CLI
5
+ module Generators
6
+ class ConnectorGenerator < Thor::Group
7
+ include Thor::Actions
8
+
9
+ argument :path, type: :string
10
+
11
+ def self.source_root
12
+ File.expand_path('../../../../templates', __dir__)
13
+ end
14
+
15
+ def create_root
16
+ self.destination_root = path
17
+
18
+ empty_directory '.'
19
+ FileUtils.cd(path)
20
+ end
21
+
22
+ def create_gemfile
23
+ template('Gemfile.erb', 'Gemfile')
24
+ end
25
+
26
+ def create_connector_file
27
+ template('connector.rb.erb', 'connector.rb')
28
+ end
29
+
30
+ def create_spec_files
31
+ template('.rspec.erb', '.rspec')
32
+
33
+ @vcr_record_mode = ask(<<~HELP)
34
+ Please select default HTTP mocking behavior suitable for your project?
35
+
36
+ 1 - secure. Cause an error to be raised for any unknown requests, all request recordings are encrypted.
37
+ To record a new cassette you need set VCR_RECORD_MODE environment variable
38
+
39
+ Example: VCR_RECORD_MODE=once bundle exec rspec spec/actions/test_action_spec.rb
40
+
41
+ 2 - simple. Record new interaction if it is a new request, requests are stored as plain text and expose secret tokens.
42
+ HELP
43
+
44
+ MasterKeyGenerator.new.call if @vcr_record_mode == '1'
45
+
46
+ template('spec/spec_helper.rb.erb', 'spec/spec_helper.rb')
47
+ template('spec/connector_spec.rb.erb', 'spec/connector_spec.rb')
48
+ end
49
+
50
+ def bundle_install
51
+ bundle_command('install')
52
+ end
53
+
54
+ def show_next_steps
55
+ say <<~HELP
56
+ The new Workato Custom Connector project created at #{path}.
57
+
58
+ Now, edit the created file. Add actions, triggers or methods and generate tests for them
59
+
60
+ cd #{path} && workato generate test
61
+ HELP
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :vcr_record_mode
67
+
68
+ def name
69
+ File.basename(path)
70
+ end
71
+
72
+ def bundle_command(command)
73
+ say_status :run, "bundle #{command}"
74
+
75
+ bundle_command = Gem.bin_path('bundler', 'bundle')
76
+
77
+ require 'bundler'
78
+ Bundler.with_original_env do
79
+ exec_bundle_command(bundle_command, command)
80
+ end
81
+ end
82
+
83
+ def exec_bundle_command(bundle_command, command)
84
+ full_command = %("#{Gem.ruby}" "#{bundle_command}" #{command})
85
+ if options[:quiet]
86
+ system(full_command, out: File::NULL)
87
+ else
88
+ system(full_command)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end