wisco 0.4.3 → 0.4.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d67bb33e37dba45fc8fe77679c3ce959d27b8c4e047359798bcd0abfa8d1b7f4
4
- data.tar.gz: 704d8a2c8935dbae8857be939638a0b9240a21bec61813adda314d5be34d5798
3
+ metadata.gz: 3ab77bb62a7ce39484d205397fb9350d3b17f2fbdde6f11ce47517da942608a1
4
+ data.tar.gz: db26dddd97fba3b9f3471d408e0f80750375d0ab18111d36d20d3b603e447daf
5
5
  SHA512:
6
- metadata.gz: 18103132c8fdd1af2dd18da30ae44fbd47097b92f4ee1b1adc9f1f515753ce357ca20b18f0a521e35e54237c10255fd7b0597d31d044664b5a27d26d663a2b2d
7
- data.tar.gz: d6a7db532c55b4eb858ca69c3bedde378948a1ad8c9d1dd2f1590afdea203c8fe788bbb8f3fd514d205746beba63d4689be14ca658406d519ef0976dd4e812d3
6
+ metadata.gz: 5eb44d4bd05b130cfc4fb3f0c59325df601230e3c7be1e1e6b40f608f9ff4202bc0a99ee51a0fff177a2db71aba9e8ecb1dccad2fc7bf1b2a8a747f15a7c0d53
7
+ data.tar.gz: 6887090edc5bf94a7c7aa8d07878738da08ac009d498e18115e65952b2598fb2ec6a98610cf80f976fdecd66d996610874bb74fb81be6c581de7a7bf3973d35f
@@ -0,0 +1,363 @@
1
+ require 'json'
2
+ require_relative '../config'
3
+ require_relative '../connector'
4
+ require_relative '../settings_store'
5
+ require_relative '../terminal_output'
6
+
7
+ module Wisco
8
+ module Commands
9
+ class Settings < Thor
10
+ def self.exit_on_failure?
11
+ true
12
+ end
13
+
14
+ desc 'list', 'List connection sets in the settings file'
15
+ option :format, type: :string, enum: %w[json], desc: 'Machine-readable output (json = array of set names)'
16
+ def list
17
+ store = settings_store
18
+ data = read_settings!(store)
19
+ names = Wisco::SettingsStore.set_names(data)
20
+
21
+ if options[:format] == 'json'
22
+ puts JSON.generate(names)
23
+ return
24
+ end
25
+
26
+ case Wisco::SettingsStore.detect_structure(data)
27
+ when :nested, :mixed
28
+ active = active_connection
29
+ puts "Connection sets (#{store.filename}):\n\n"
30
+ width = names.map(&:length).max || 0
31
+ names.each do |n|
32
+ marker = n == active ? '*' : ' '
33
+ puts " #{marker} #{n.ljust(width)}"
34
+ end
35
+ if active && !active.empty?
36
+ puts
37
+ puts "Active connection (from #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME}): #{active}"
38
+ end
39
+ when :flat
40
+ puts "#{store.filename} contains a single, unnamed connection set."
41
+ puts "No named connection sets are defined. Use 'wisco settings add <name>' to create named sets."
42
+ when :none
43
+ puts "No settings file found in #{store.connector_path}."
44
+ puts "Run 'wisco settings add <name>' to create one, or 'workato edit' to create an encrypted file."
45
+ end
46
+ end
47
+
48
+ desc 'set CONNECTION', "Set the project's active connection set"
49
+ def set(connection)
50
+ store = settings_store
51
+ names = begin
52
+ Wisco::SettingsStore.set_names(store.read_all)
53
+ rescue Wisco::SettingsStore::MissingKeyError
54
+ nil
55
+ end
56
+
57
+ if names.nil?
58
+ Wisco::TerminalOutput.emit_warning("Warning: could not read #{store.filename} to validate (no master key).")
59
+ Wisco::TerminalOutput.emit_warning(' Config updated anyway.')
60
+ elsif !names.include?(connection)
61
+ Wisco::TerminalOutput.emit_warning("Warning: \"#{connection}\" is not currently defined in #{store.filename}.")
62
+ Wisco::TerminalOutput.emit_warning(" Defined sets: #{names.empty? ? '(none)' : names.join(', ')}")
63
+ Wisco::TerminalOutput.emit_warning(" Config updated anyway. Run 'wisco settings add #{connection}' to create it.")
64
+ end
65
+
66
+ cfg = config
67
+ cfg['connection'] = connection
68
+ Wisco::Config.save_config(config_path, cfg)
69
+ puts "Active connection set to \"#{connection}\" in #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME}."
70
+ end
71
+
72
+ desc 'add CONNECTION', 'Scaffold a new connection set from connector.connection.fields'
73
+ long_desc <<~DESC
74
+ Reads connection.fields from the connector and writes a new named connection
75
+ set with those field names and blank values. Fill the values in afterwards
76
+ with 'workato edit'. If the settings file currently holds a single unnamed
77
+ (flat) connection set, you are prompted to name it so the file can be
78
+ converted to the named (nested) format.
79
+ DESC
80
+ def add(connection)
81
+ field_names = connection_field_names!
82
+ if field_names.empty?
83
+ Wisco::TerminalOutput.emit_error('Error: The connector defines no connection.fields to scaffold.')
84
+ exit 1
85
+ end
86
+ new_set = field_names.each_with_object({}) { |n, h| h[n] = '' }
87
+
88
+ store = settings_store
89
+ data = read_settings!(store)
90
+ structure = Wisco::SettingsStore.detect_structure(data)
91
+
92
+ if Wisco::SettingsStore.set_names(data).include?(connection)
93
+ Wisco::TerminalOutput.emit_error("Error: Connection set \"#{connection}\" already exists in #{store.filename}.")
94
+ Wisco::TerminalOutput.emit_error(" Use 'wisco settings show #{connection}' to view it, or edit it with 'workato edit'.")
95
+ exit 1
96
+ end
97
+
98
+ target =
99
+ case structure
100
+ when :nested
101
+ data.merge(connection => new_set)
102
+ when :none
103
+ { connection => new_set }
104
+ when :flat
105
+ migrate_flat(data, store).merge(connection => new_set)
106
+ when :mixed
107
+ Wisco::TerminalOutput.emit_error("Error: #{store.filename} mixes flat keys and named sets; resolve it with 'workato edit' before adding.")
108
+ exit 1
109
+ end
110
+
111
+ # A brand-new file is written plaintext; existing files keep their form.
112
+ encrypted = structure != :none && store.encrypted?
113
+ written = encrypted ? Wisco::SettingsStore::ENCRYPTED_FILENAME : Wisco::SettingsStore::PLAINTEXT_FILENAME
114
+ store.write_all(target, encrypted: encrypted)
115
+
116
+ puts "Added connection set \"#{connection}\" to #{written} with #{field_names.length} blank field(s):"
117
+ puts " #{field_names.join(', ')}"
118
+ puts "Fill in the values with 'workato edit', then run 'wisco settings set #{connection}'."
119
+ end
120
+
121
+ desc 'show [CONNECTION]', "Show a connection set's field values (passwords masked)"
122
+ option :format, type: :string, enum: %w[json], desc: 'Machine-readable output (json = connection fields with current values)'
123
+ def show(connection = nil)
124
+ store = settings_store
125
+ data = read_settings!(store)
126
+
127
+ name, set =
128
+ case Wisco::SettingsStore.detect_structure(data)
129
+ when :none
130
+ Wisco::TerminalOutput.emit_error("Error: No settings file found in #{store.connector_path}. Nothing to show.")
131
+ exit 1
132
+ when :flat
133
+ if connection
134
+ Wisco::TerminalOutput.emit_error("Error: #{store.filename} has a single unnamed connection set; there is no named set \"#{connection}\".")
135
+ exit 1
136
+ end
137
+ [nil, data]
138
+ when :nested, :mixed
139
+ names = Wisco::SettingsStore.set_names(data)
140
+ if connection.nil?
141
+ Wisco::TerminalOutput.emit_warning('Warning: This settings file has multiple connection sets; specify which one to show.')
142
+ Wisco::TerminalOutput.emit_warning(" Defined sets: #{names.join(', ')}")
143
+ return
144
+ end
145
+ unless names.include?(connection)
146
+ Wisco::TerminalOutput.emit_error("Error: Connection set \"#{connection}\" not found in #{store.filename}.")
147
+ Wisco::TerminalOutput.emit_error(" Defined sets: #{names.join(', ')}")
148
+ exit 1
149
+ end
150
+ [connection, data[connection]]
151
+ end
152
+
153
+ if options[:format] == 'json'
154
+ render_set_json(set)
155
+ else
156
+ render_set(name, set, store)
157
+ end
158
+ end
159
+
160
+ desc 'current', 'Show which connection set the project points at'
161
+ def current
162
+ active = active_connection.to_s.strip
163
+ store = settings_store
164
+ data = begin
165
+ store.read_all
166
+ rescue Wisco::SettingsStore::MissingKeyError
167
+ nil
168
+ end
169
+ names = data ? Wisco::SettingsStore.set_names(data) : []
170
+
171
+ if active.empty?
172
+ puts 'This project has no named connection selected (config.json has no "connection" key).'
173
+ puts "The connector will use the single connection set in #{store.filename}."
174
+ return
175
+ end
176
+
177
+ puts "This project uses connection set: #{active}"
178
+ if data.nil?
179
+ Wisco::TerminalOutput.emit_warning(" Could not read #{store.filename} to verify (no master key).")
180
+ elsif names.include?(active)
181
+ puts " Defined in: #{store.filename} ✓"
182
+ else
183
+ puts " Not found in #{store.filename}. Run 'wisco settings add #{active}' to create it."
184
+ end
185
+ end
186
+
187
+ desc 'fields', 'List the connector connection fields'
188
+ option :format, type: :string, enum: %w[json], desc: 'Machine-readable output (json)'
189
+ def fields
190
+ raw = connection_fields!
191
+
192
+ if options[:format] == 'json'
193
+ puts JSON.pretty_generate(raw)
194
+ return
195
+ end
196
+
197
+ if raw.empty?
198
+ puts 'Connector defines no connection fields.'
199
+ return
200
+ end
201
+
202
+ rows = raw.map do |f|
203
+ [
204
+ field_attr(f, :name).to_s,
205
+ field_attr(f, :label).to_s,
206
+ (field_attr(f, :control_type) || '(default)').to_s,
207
+ field_attr(f, :optional) ? 'no' : 'yes'
208
+ ]
209
+ end
210
+
211
+ headers = %w[Name Label Type Required]
212
+ widths = headers.each_index.map do |i|
213
+ ([headers[i]] + rows.map { |r| r[i] }).map(&:length).max
214
+ end
215
+
216
+ puts "Connection fields (from #{connector_file_basename}):\n\n"
217
+ puts " #{headers.each_with_index.map { |h, i| h.ljust(widths[i]) }.join(' ')}"
218
+ rows.each do |r|
219
+ puts " #{r.each_with_index.map { |c, i| c.ljust(widths[i]) }.join(' ')}"
220
+ end
221
+ end
222
+
223
+ no_commands do
224
+ def target_dir
225
+ Dir.pwd
226
+ end
227
+
228
+ def config_path
229
+ Wisco.config_path(target_dir)
230
+ end
231
+
232
+ def config
233
+ @config ||= begin
234
+ unless File.exist?(config_path)
235
+ Wisco::TerminalOutput.emit_error("Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{target_dir}.")
236
+ Wisco::TerminalOutput.emit_error(" Run 'wisco init' first.")
237
+ exit 1
238
+ end
239
+ Wisco::Config.load_config(config_path)
240
+ end
241
+ end
242
+
243
+ def connector_path
244
+ path = config.dig('connector', 'path')
245
+ if path.nil?
246
+ Wisco::TerminalOutput.emit_error("Error: #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} is missing connector path. Run 'wisco init' again.")
247
+ exit 1
248
+ end
249
+ path
250
+ end
251
+
252
+ def connector_file_basename
253
+ config.dig('connector', 'file') || 'connector.rb'
254
+ end
255
+
256
+ def active_connection
257
+ config['connection']
258
+ end
259
+
260
+ def settings_store
261
+ Wisco::SettingsStore.new(connector_path)
262
+ end
263
+
264
+ # Reads the settings file, converting a missing master key into a clean
265
+ # error+exit (used by commands that cannot proceed without the contents).
266
+ def read_settings!(store)
267
+ store.read_all
268
+ rescue Wisco::SettingsStore::MissingKeyError => e
269
+ Wisco::TerminalOutput.emit_error("Error: #{e.message}")
270
+ exit 1
271
+ end
272
+
273
+ # connection.fields array from the connector (symbol-keyed hashes).
274
+ # Exits with the connector's load error if it cannot be loaded.
275
+ def connection_fields!
276
+ connector = Wisco::Connector.load_connector_from_config(target_dir)
277
+ Array(connector.is_a?(Hash) ? connector.dig(:connection, :fields) : nil)
278
+ rescue StandardError => e
279
+ Wisco::TerminalOutput.emit_error("Error: #{e.message.strip}")
280
+ exit 1
281
+ end
282
+
283
+ def connection_field_names!
284
+ connection_fields!.map { |f| field_attr(f, :name).to_s }.reject(&:empty?)
285
+ end
286
+
287
+ # Password field names for masking. Degrades gracefully (returns []) if
288
+ # the connector cannot be loaded, so `show` still works.
289
+ def password_field_names
290
+ connector = Wisco::Connector.load_connector_from_config(target_dir)
291
+ fields = Array(connector.is_a?(Hash) ? connector.dig(:connection, :fields) : nil)
292
+ fields.select { |f| field_attr(f, :control_type).to_s == 'password' }
293
+ .map { |f| field_attr(f, :name).to_s }
294
+ rescue SystemExit, StandardError
295
+ Wisco::TerminalOutput.emit_warning('Warning: could not load connector to identify password fields; showing all values unmasked.')
296
+ []
297
+ end
298
+
299
+ def field_attr(field, key)
300
+ return nil unless field.is_a?(Hash)
301
+
302
+ field[key] || field[key.to_s]
303
+ end
304
+
305
+ def render_set(name, set, store)
306
+ passwords = password_field_names
307
+ header = name || '(single/unnamed)'
308
+ puts "Connection set: #{header} (#{store.filename})\n\n"
309
+
310
+ if set.nil? || set.empty?
311
+ puts ' (no fields)'
312
+ return
313
+ end
314
+
315
+ width = set.keys.map { |k| k.to_s.length }.max
316
+ set.each do |k, v|
317
+ display =
318
+ if v.to_s.empty?
319
+ '(blank)'
320
+ elsif passwords.include?(k.to_s)
321
+ mask(v.to_s)
322
+ else
323
+ v.to_s
324
+ end
325
+ puts " #{k.to_s.ljust(width)} #{display}"
326
+ end
327
+ end
328
+
329
+ def mask(value)
330
+ return '****' if value.length <= 4
331
+
332
+ "****#{value[-4..]}"
333
+ end
334
+
335
+ # Machine-readable form of a connection set: the connector's
336
+ # connection.fields array, each field augmented with a `value` key holding
337
+ # the current stored value (null when the field is unset or blank). Values
338
+ # are unmasked — this output is for tooling, not display.
339
+ def render_set_json(set)
340
+ set ||= {}
341
+ out = connection_fields!.map do |f|
342
+ raw = set[field_attr(f, :name).to_s]
343
+ value = raw.nil? || raw.to_s.empty? ? nil : raw
344
+ f.merge(value: value)
345
+ end
346
+ puts JSON.pretty_generate(out)
347
+ end
348
+
349
+ def migrate_flat(flat_data, store)
350
+ puts "#{store.filename} currently holds a single unnamed connection set."
351
+ puts 'Adding a named set requires converting the file to the named (nested) format.'
352
+ print 'Enter a name for the existing connection set (or press Enter to skip): '
353
+ name = $stdin.gets.to_s.strip
354
+ if name.empty?
355
+ Wisco::TerminalOutput.emit_error('Aborted: mixing flat and named connection sets is not supported.')
356
+ exit 1
357
+ end
358
+ { name => flat_data }
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,133 @@
1
+ require 'yaml'
2
+ require 'active_support/encrypted_configuration'
3
+ require 'workato/connector/sdk'
4
+ require_relative 'terminal_output'
5
+
6
+ module Wisco
7
+ # Read/write wrapper over the Workato SDK settings file (settings.yaml /
8
+ # settings.yaml.enc) living in a connector directory. Handles encrypted vs
9
+ # plaintext auto-detection, master-key resolution, and flat-vs-nested
10
+ # connection-set detection. See doc-specs/wisco-10-settings.md.
11
+ class SettingsStore
12
+ PLAINTEXT_FILENAME = 'settings.yaml'.freeze
13
+ ENCRYPTED_FILENAME = 'settings.yaml.enc'.freeze
14
+ MASTER_KEY_FILENAME = 'master.key'.freeze
15
+ MASTER_KEY_ENV = 'WORKATO_CONNECTOR_MASTER_KEY'.freeze
16
+
17
+ class MissingKeyError < StandardError; end
18
+
19
+ attr_reader :connector_path
20
+
21
+ def initialize(connector_path)
22
+ @connector_path = File.expand_path(connector_path)
23
+ end
24
+
25
+ def plaintext_path
26
+ File.join(@connector_path, PLAINTEXT_FILENAME)
27
+ end
28
+
29
+ def encrypted_path
30
+ File.join(@connector_path, ENCRYPTED_FILENAME)
31
+ end
32
+
33
+ def master_key_path
34
+ File.join(@connector_path, MASTER_KEY_FILENAME)
35
+ end
36
+
37
+ # :encrypted (settings.yaml.enc present), :plaintext (settings.yaml present),
38
+ # or :none. Encrypted takes precedence when both exist.
39
+ def form
40
+ if File.exist?(encrypted_path)
41
+ :encrypted
42
+ elsif File.exist?(plaintext_path)
43
+ :plaintext
44
+ else
45
+ :none
46
+ end
47
+ end
48
+
49
+ def encrypted?
50
+ form == :encrypted
51
+ end
52
+
53
+ def exist?
54
+ form != :none
55
+ end
56
+
57
+ # Basename for user-facing messages; falls back to the plaintext name when
58
+ # no file exists yet (that is the form `add` would create).
59
+ def filename
60
+ encrypted? ? ENCRYPTED_FILENAME : PLAINTEXT_FILENAME
61
+ end
62
+
63
+ def master_key_available?
64
+ !ENV[MASTER_KEY_ENV].to_s.strip.empty? || File.exist?(master_key_path)
65
+ end
66
+
67
+ # Reads the entire settings file (all sets) as a plain string-keyed Hash.
68
+ # Returns {} when no file exists. Raises MissingKeyError if the file is
69
+ # encrypted and no master key is available.
70
+ def read_all
71
+ case form
72
+ when :none
73
+ {}
74
+ when :plaintext
75
+ parsed = YAML.safe_load(File.read(plaintext_path), permitted_classes: [::Symbol])
76
+ parsed.is_a?(Hash) ? parsed : {}
77
+ when :encrypted
78
+ require_master_key!
79
+ raw = Workato::Connector::Sdk::Settings.from_encrypted_file(encrypted_path, master_key_path)
80
+ raw.respond_to?(:to_hash) ? raw.to_hash : {}
81
+ end
82
+ end
83
+
84
+ # Replaces the whole settings file with `data`. When `encrypted:` is true the
85
+ # content is encrypted with the resolved master key; otherwise a plaintext
86
+ # settings.yaml is written. Rewriting wholesale (rather than the SDK's
87
+ # merge-only #update) is what lets `add` migrate a flat file to nested form.
88
+ def write_all(data, encrypted:)
89
+ if encrypted
90
+ require_master_key!
91
+ config = ActiveSupport::EncryptedConfiguration.new(
92
+ config_path: encrypted_path,
93
+ key_path: master_key_path,
94
+ env_key: MASTER_KEY_ENV,
95
+ raise_if_missing_key: true
96
+ )
97
+ config.write(YAML.dump(data))
98
+ else
99
+ File.write(plaintext_path, YAML.dump(data))
100
+ end
101
+ end
102
+
103
+ def require_master_key!
104
+ return if master_key_available?
105
+
106
+ raise MissingKeyError,
107
+ "#{ENCRYPTED_FILENAME} is encrypted but no master key was found.\n" \
108
+ " Set #{MASTER_KEY_ENV} or provide a #{MASTER_KEY_FILENAME} file in #{@connector_path}."
109
+ end
110
+
111
+ # :none, :flat (all top-level values scalar), :nested (all Hash), or :mixed.
112
+ def self.detect_structure(data)
113
+ return :none if data.nil? || data.empty?
114
+
115
+ values = data.values
116
+ if values.all? { |v| v.is_a?(Hash) }
117
+ :nested
118
+ elsif values.none? { |v| v.is_a?(Hash) }
119
+ :flat
120
+ else
121
+ :mixed
122
+ end
123
+ end
124
+
125
+ # Names of the named (nested) connection sets — the top-level keys whose
126
+ # value is a Hash. Empty for flat/none files.
127
+ def self.set_names(data)
128
+ return [] if data.nil?
129
+
130
+ data.select { |_k, v| v.is_a?(Hash) }.keys
131
+ end
132
+ end
133
+ end
data/lib/wisco/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Wisco
2
- VERSION = '0.4.3'
2
+ VERSION = '0.4.4'
3
3
  end
data/lib/wisco.rb CHANGED
@@ -25,6 +25,7 @@ require_relative 'wisco/commands/pull'
25
25
  require_relative 'wisco/commands/push'
26
26
  require_relative 'wisco/commands/schema'
27
27
  require_relative 'wisco/commands/profile'
28
+ require_relative 'wisco/commands/settings'
28
29
  require_relative 'wisco/commands/status'
29
30
 
30
31
  module Wisco
@@ -217,6 +218,18 @@ module Wisco
217
218
  DESC
218
219
  subcommand 'profile', Wisco::Commands::Profile
219
220
 
221
+ desc 'settings SUBCOMMAND ...ARGS', 'Manage the connector settings file (credential sets)'
222
+ long_desc <<~DESC
223
+ Subcommands:
224
+ list List connection sets (--format=json for a JSON array of names)
225
+ set <connection> Set the project's active connection set
226
+ add <connection> Scaffold a new connection set from connector.connection.fields
227
+ show [<connection>] Show a connection set's field values (passwords masked)
228
+ current Show which connection set the project points at
229
+ fields List the connector's connection fields (--format=json for JSON)
230
+ DESC
231
+ subcommand 'settings', Wisco::Commands::Settings
232
+
220
233
  desc 'schema INPUT_FILE [TARGET_DIR]', 'Generate a schema from a JSON or CSV sample file'
221
234
  long_desc <<~DESC
222
235
  Calls the Workato API to generate a schema from a sample JSON or CSV file.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wisco
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - mbillington
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-26 00:00:00.000000000 Z
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -75,12 +75,14 @@ files:
75
75
  - lib/wisco/commands/pull.rb
76
76
  - lib/wisco/commands/push.rb
77
77
  - lib/wisco/commands/schema.rb
78
+ - lib/wisco/commands/settings.rb
78
79
  - lib/wisco/commands/status.rb
79
80
  - lib/wisco/config.rb
80
81
  - lib/wisco/connector.rb
81
82
  - lib/wisco/exec_script.rb
82
83
  - lib/wisco/path_utils.rb
83
84
  - lib/wisco/profile.rb
85
+ - lib/wisco/settings_store.rb
84
86
  - lib/wisco/terminal_output.rb
85
87
  - lib/wisco/version.rb
86
88
  - lib/wisco/workato_api.rb