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