wisco 0.1.7

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 832da4703274d86e1bc2f5e1684e499b9c4f2de2628c3f0dfb362e23e34213b9
4
+ data.tar.gz: 369be3677fdc543c2a0d1a5344e195bf97e05a61d1a9816b6f1993bc521c63af
5
+ SHA512:
6
+ metadata.gz: 3c5e906f59c4f5db77e30a8c583e09cea24e02074c89dfb1b9f985a7c3078bf789f17e5723d49d303789f8b0328d1c5a2c8bf80b735ea1706c4834700eef8dcb
7
+ data.tar.gz: e474a3f6f3b761f3165366fb335c276f7d73ed2725d50a7dc2bea9fde92decf13b10991a809bc86b2737dbbc558284e8617c4301f321c67dadfe00e127e68eee
data/bin/wisco ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ if Gem.win_platform?
3
+ $stdout.binmode
4
+ $stdout.set_encoding('UTF-8')
5
+ end
6
+ require 'wisco'
7
+ Wisco::CLI.start(ARGV)
@@ -0,0 +1,126 @@
1
+ require 'fileutils'
2
+ require 'workato/connector/sdk'
3
+ require 'workato/cli/exec_command'
4
+ require_relative '../config'
5
+ require_relative '../connector'
6
+ require_relative '../path_utils'
7
+
8
+ module Wisco
9
+ module Commands
10
+ module Exec
11
+ module_function
12
+
13
+ def run(path_arg, target_dir, input: nil, debug: false)
14
+ target_dir = File.expand_path(target_dir)
15
+ config_path = Wisco.config_path(target_dir)
16
+
17
+ unless File.exist?(config_path)
18
+ warn "Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{target_dir}."
19
+ warn " Run '#{Wisco::CLI_NAME} init' first."
20
+ exit 1
21
+ end
22
+
23
+ config = Wisco::Config.load_config(config_path)
24
+ connector_path = config.dig('connector', 'path')
25
+ connector_file = config.dig('connector', 'file')
26
+
27
+ if connector_path.nil? || connector_file.nil?
28
+ warn "Error: #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} is missing connector path/file. Run '#{Wisco::CLI_NAME} init' again."
29
+ exit 1
30
+ end
31
+
32
+ connector_full_path = File.join(connector_path, connector_file)
33
+ connection = config['connection']
34
+
35
+ connector = Wisco::Connector.load_connector_from_config(target_dir)
36
+ pairs = Wisco::PathUtils.parse_path(path_arg, connector)
37
+
38
+ pairs.each do |section, key|
39
+ puts "Executing #{section}.#{key}"
40
+ fixtures_dir = File.join(target_dir, 'fixtures', section, key)
41
+ fixture_dir_output = fixtures_dir.sub(connector_path, '.')
42
+
43
+ unless File.directory?(fixtures_dir)
44
+ warn "Error: fixtures directory not found: #{fixture_dir_output}"
45
+ warn " Run '#{Wisco::CLI_NAME} fixtures #{section}.#{key}' first."
46
+ next
47
+ end
48
+
49
+ input_files = resolve_input_files(input, fixtures_dir)
50
+
51
+ if input_files.empty?
52
+ warn "\tWarning: No ready input files found in #{fixture_dir_output}"
53
+ next
54
+ end
55
+
56
+ input_files.each do |input_file|
57
+ execute_one(section, key, input_file, fixtures_dir,
58
+ connector_full_path, connection, debug: debug)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Resolve the list of input files to execute.
64
+ # If an explicit input filename/path is given, use that (relative to fixtures_dir).
65
+ # Otherwise glob execute_* in fixtures_dir and exclude files still containing the sentinel.
66
+ def resolve_input_files(input, fixtures_dir)
67
+ if input
68
+ path = File.absolute_path?(input) ? input : File.join(fixtures_dir, input)
69
+ unless File.exist?(path)
70
+ warn "Error: Input file not found: #{path}"
71
+ exit 1
72
+ end
73
+ [path]
74
+ else
75
+ Dir.glob(File.join(fixtures_dir, 'execute_*')).select do |f|
76
+ File.file?(f) && !file_has_sentinel?(f)
77
+ end
78
+ end
79
+ end
80
+
81
+ def file_has_sentinel?(path)
82
+ first_line = begin
83
+ File.open(path, &:readline).chomp
84
+ rescue StandardError
85
+ ''
86
+ end
87
+ first_line == Wisco::Commands::Fixtures::SENTINEL
88
+ end
89
+
90
+ def execute_one(section, key, input_file, fixtures_dir, connector_full_path, connection, debug: false)
91
+ stem = File.basename(input_file, '.*')
92
+ output_file = File.join(fixtures_dir, "output_#{stem}.json")
93
+ error_file = File.join(fixtures_dir, "error_#{stem}.txt")
94
+
95
+ options = { connector: connector_full_path, input: input_file, output: output_file }
96
+ options[:connection] = connection if connection
97
+
98
+ if debug
99
+ warn "[exec] path: #{section}.#{key}.execute"
100
+ warn "[exec] connector: #{connector_full_path}"
101
+ warn "[exec] connection: #{connection.inspect}"
102
+ warn "[exec] input: #{input_file}"
103
+ warn "[exec] output: #{output_file}"
104
+ end
105
+
106
+ begin
107
+ cmd = Workato::CLI::ExecCommand.new(path: "#{section}.#{key}.execute", options: options)
108
+ cmd.call
109
+ rescue StandardError => e
110
+ File.write(error_file, "#{e.class}: #{e.message}\n\n#{e.backtrace.join("\n")}\n")
111
+ warn "Error executing #{section}.#{key} with #{File.basename(input_file)}: #{e.message}"
112
+ warn " Details written to: #{error_file}"
113
+ return
114
+ end
115
+
116
+ FileUtils.rm_f(error_file)
117
+
118
+ return unless File.exist?(output_file)
119
+
120
+ pretty = JSON.pretty_generate(JSON.parse(File.read(output_file)))
121
+ File.write(output_file, pretty + "\n")
122
+ puts " Written: #{output_file}"
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,149 @@
1
+ require 'fileutils'
2
+ require 'workato/connector/sdk'
3
+ require 'workato/cli/exec_command'
4
+ require_relative '../config'
5
+ require_relative '../connector'
6
+ require_relative '../path_utils'
7
+
8
+ module Wisco
9
+ module Commands
10
+ module Fixtures
11
+ SENTINEL = '# Remove this comment before updating. Files that include this line will be overwritten.'
12
+
13
+ module_function
14
+
15
+ def run(path_arg, target_dir, overwrite: false, debug: false)
16
+ target_dir = File.expand_path(target_dir)
17
+ config_path = Wisco.config_path(target_dir)
18
+
19
+ unless File.exist?(config_path)
20
+ warn "Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{target_dir}."
21
+ warn " Run '#{Wisco::CLI_NAME} init' first."
22
+ exit 1
23
+ end
24
+
25
+ config = Wisco::Config.load_config(config_path)
26
+ connector_path = config.dig('connector', 'path')
27
+ connector_file = config.dig('connector', 'file')
28
+
29
+ if connector_path.nil? || connector_file.nil?
30
+ warn "Error: #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} is missing connector path/file. Run '#{Wisco::CLI_NAME} init' again."
31
+ exit 1
32
+ end
33
+
34
+ connector_full_path = File.join(connector_path, connector_file)
35
+ connection = config['connection']
36
+
37
+ connector = Wisco::Connector.load_connector_from_config(target_dir)
38
+ pairs = Wisco::PathUtils.parse_path(path_arg, connector)
39
+
40
+ pairs.each do |section, key|
41
+ puts "Processing #{section}.#{key}"
42
+ fixtures_dir = File.join(target_dir, 'fixtures', section, key)
43
+ FileUtils.mkdir_p(fixtures_dir)
44
+
45
+ input_fields_file = File.join(fixtures_dir, 'input_fields.json')
46
+ call_exec(
47
+ path: "#{section}.#{key}.input_fields",
48
+ connector: connector_full_path,
49
+ connection: connection,
50
+ output: input_fields_file,
51
+ debug: debug
52
+ )
53
+
54
+ generate_execute_input(input_fields_file, fixtures_dir, overwrite: overwrite, debug: debug)
55
+
56
+ output_fields_file = File.join(fixtures_dir, 'output_fields.json')
57
+ call_exec(
58
+ path: "#{section}.#{key}.output_fields",
59
+ connector: connector_full_path,
60
+ connection: connection,
61
+ output: output_fields_file,
62
+ debug: debug
63
+ )
64
+ end
65
+ end
66
+
67
+ # Reads input_fields.json, builds a template hash, writes execute_input.json.
68
+ # The file is prefixed with SENTINEL so it is identifiable as an unedited template.
69
+ # Overwrite rules:
70
+ # - File absent -> write
71
+ # - File present, sentinel L1 -> overwrite (still a template)
72
+ # - File present, no sentinel -> skip (user-edited); force with --overwrite
73
+ def generate_execute_input(input_fields_file, fixtures_dir, overwrite: false, debug: false)
74
+ return unless File.exist?(input_fields_file)
75
+
76
+ fields = JSON.parse(File.read(input_fields_file))
77
+ return if fields.empty?
78
+
79
+ output_file = File.join(fixtures_dir, 'execute_input.json')
80
+
81
+ if File.exist?(output_file)
82
+ first_line = begin
83
+ File.open(output_file, &:readline).chomp
84
+ rescue StandardError
85
+ ''
86
+ end
87
+ has_sentinel = (first_line == SENTINEL)
88
+
89
+ unless has_sentinel || overwrite
90
+ puts " Skipped (user-edited): #{output_file}" if debug
91
+ return
92
+ end
93
+ end
94
+
95
+ template = schema_to_template(fields)
96
+ content = "#{SENTINEL}\n#{JSON.pretty_generate(template)}\n"
97
+ File.write(output_file, content)
98
+ puts " Written: #{output_file}"
99
+ end
100
+
101
+ # Recursively converts a Workato schema array into a template hash.
102
+ # Scalars become "<type_value_required|optional>" placeholder strings.
103
+ # Objects expand into a nested hash via their properties.
104
+ # Arrays expand into a single-element array via their properties.
105
+ def schema_to_template(fields)
106
+ fields.each_with_object({}) do |field, hash|
107
+ name = field['name']
108
+ type = field['type'] || 'string'
109
+ optional = field.fetch('optional', true)
110
+ req_str = optional ? 'optional' : 'required'
111
+
112
+ hash[name] = case type
113
+ when 'object'
114
+ schema_to_template(field['properties'] || [])
115
+ when 'array'
116
+ [schema_to_template(field['properties'] || [])]
117
+ else
118
+ "<#{type}_value_#{req_str}>"
119
+ end
120
+ end
121
+ end
122
+
123
+ def call_exec(path:, connector:, connection:, output:, debug: false)
124
+ options = { connector: connector, output: output }
125
+ options[:connection] = connection if connection
126
+
127
+ if debug
128
+ warn "[fixtures] path: #{path}"
129
+ warn "[fixtures] connector: #{connector}"
130
+ warn "[fixtures] connection: #{connection.inspect}"
131
+ warn "[fixtures] output: #{output}"
132
+ end
133
+
134
+ cmd = Workato::CLI::ExecCommand.new(path: path, options: options)
135
+ begin
136
+ cmd.call
137
+ rescue StandardError => e
138
+ warn " Warning: #{path} failed — #{e.message}"
139
+ return
140
+ end
141
+
142
+ return unless File.exist?(output)
143
+
144
+ pretty = JSON.pretty_generate(JSON.parse(File.read(output)))
145
+ File.write(output, pretty + "\n")
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,63 @@
1
+ require 'fileutils'
2
+ require_relative '../config'
3
+ require_relative '../connector'
4
+
5
+ module Wisco
6
+ module Commands
7
+ module Init
8
+ module_function
9
+
10
+ def run(target_dir)
11
+ target_dir = File.expand_path(target_dir)
12
+
13
+ unless Dir.exist?(target_dir)
14
+ warn "Error: Directory not found: #{target_dir}"
15
+ exit 1
16
+ end
17
+
18
+ puts "Searching for connector in #{target_dir}..."
19
+
20
+ connector_file = Wisco::Connector.detect_connector(target_dir)
21
+
22
+ if connector_file.nil?
23
+ warn "Error: No valid Workato connector file found in #{target_dir}"
24
+ exit 1
25
+ end
26
+
27
+ puts "Found connector: #{connector_file}"
28
+
29
+ wisco_dir = File.join(target_dir, Wisco::WISCO_DIR)
30
+ FileUtils.mkdir_p(wisco_dir)
31
+
32
+ config_path = Wisco.config_path(target_dir)
33
+ config = Wisco::Config.load_config(config_path)
34
+
35
+ config['connector'] ||= {}
36
+ config['connector']['path'] = target_dir
37
+ config['connector']['file'] = connector_file
38
+
39
+ Wisco::Config.save_config(config_path, config)
40
+ puts "Config written to #{config_path}"
41
+
42
+ update_gitignore(target_dir)
43
+ end
44
+
45
+ def update_gitignore(target_dir)
46
+ gitignore_path = File.join(target_dir, '.gitignore')
47
+ entry = "#{Wisco::WISCO_DIR}/"
48
+
49
+ if File.exist?(gitignore_path)
50
+ content = File.read(gitignore_path)
51
+ if content.include?(entry)
52
+ puts ".gitignore already contains '#{entry}' — no changes made."
53
+ else
54
+ File.open(gitignore_path, 'a') { |f| f.puts entry }
55
+ puts "Added '#{entry}' to .gitignore"
56
+ end
57
+ else
58
+ puts "Note: No .gitignore found — consider adding '#{entry}' manually."
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,192 @@
1
+ require_relative '../connector'
2
+
3
+ module Wisco
4
+ module Commands
5
+ module List
6
+ TREE_FORK = '├── '
7
+ TREE_LAST = '└── '
8
+ TREE_PIPE = '│ '
9
+ TREE_BLANK = ' '
10
+ EXPANDABLE_KEYS = %i[actions triggers object_definitions methods pick_lists].freeze
11
+ SORT_FIELDS = %w[key title].freeze
12
+
13
+ module_function
14
+
15
+ def run(subcommand, target, sort: nil)
16
+ validate_sort!(sort)
17
+
18
+ case subcommand
19
+ when nil then run_tree(target)
20
+ when 'actions' then run_actions(target, sort: sort)
21
+ when 'triggers' then run_triggers(target, sort: sort)
22
+ when 'all' then run_all(target, sort: sort)
23
+ else
24
+ warn "Error: Unknown list subcommand '#{subcommand}'"
25
+ warn "Run '#{Wisco::CLI_NAME} --help' for usage."
26
+ exit 1
27
+ end
28
+ end
29
+
30
+ def run_tree(target_dir)
31
+ connector = Wisco::Connector.load_connector_from_config(target_dir)
32
+ puts connector[:title]
33
+
34
+ keys = connector.keys
35
+ keys.each_with_index do |key, idx|
36
+ last = idx == keys.size - 1
37
+ connector_str = last ? TREE_LAST : TREE_FORK
38
+ child_prefix = last ? TREE_BLANK : TREE_PIPE
39
+ value = connector[key]
40
+
41
+ if key == :connection && value.is_a?(Hash)
42
+ puts "#{connector_str}#{key}"
43
+ render_connection_tree(value, child_prefix)
44
+ elsif EXPANDABLE_KEYS.include?(key) && value.is_a?(Hash) && !value.empty?
45
+ puts "#{connector_str}#{key} [#{value.size}]"
46
+ render_children_tree(value, child_prefix)
47
+ elsif value.is_a?(Hash)
48
+ puts "#{connector_str}#{key} [#{value.size}]"
49
+ elsif value.is_a?(Array)
50
+ puts "#{connector_str}#{key} [#{value.size}]"
51
+ else
52
+ puts "#{connector_str}#{key}"
53
+ end
54
+ end
55
+ end
56
+
57
+ def run_actions(target_dir, sort: nil)
58
+ connector = Wisco::Connector.load_connector_from_config(target_dir)
59
+ actions = connector[:actions]
60
+
61
+ if actions.nil? || actions.empty?
62
+ puts 'No actions defined.'
63
+ return
64
+ end
65
+
66
+ rows = actions.map { |key, item| [key, title_for(key, item), subtitle_for(item)] }
67
+ rows = sort_rows(rows, sort)
68
+ render_markdown_table(%w[Key Title Subtitle], rows)
69
+ end
70
+
71
+ def run_triggers(target_dir, sort: nil)
72
+ connector = Wisco::Connector.load_connector_from_config(target_dir)
73
+ triggers = connector[:triggers]
74
+
75
+ if triggers.nil? || triggers.empty?
76
+ puts 'No triggers defined.'
77
+ return
78
+ end
79
+
80
+ rows = triggers.map { |key, item| [key, title_for(key, item), subtitle_for(item)] }
81
+ rows = sort_rows(rows, sort)
82
+ render_markdown_table(%w[Key Title Subtitle], rows)
83
+ end
84
+
85
+ def run_all(target_dir, sort: nil)
86
+ puts '## Overview'
87
+ run_tree(target_dir)
88
+ puts
89
+ puts '## Actions'
90
+ run_actions(target_dir, sort: sort)
91
+ puts
92
+ puts '## Triggers'
93
+ run_triggers(target_dir, sort: sort)
94
+ end
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # List utilities
98
+ # ---------------------------------------------------------------------------
99
+
100
+ def strip_html(str)
101
+ str.to_s.gsub(/<[^>]+>/, '').squeeze(' ').strip
102
+ end
103
+
104
+ def humanise_key(key)
105
+ key.to_s.split('_').map(&:capitalize).join(' ')
106
+ end
107
+
108
+ def title_for(key, item)
109
+ item.is_a?(Hash) && item[:title] ? item[:title] : humanise_key(key)
110
+ end
111
+
112
+ def subtitle_for(item)
113
+ return '' unless item.is_a?(Hash)
114
+ return item[:subtitle] if item[:subtitle]
115
+
116
+ strip_html(item[:description]) if item[:description]
117
+ item[:subtitle] || strip_html(item[:description].to_s)
118
+ end
119
+
120
+ def render_markdown_table(headers, rows)
121
+ all_rows = [headers] + rows
122
+ widths = headers.length.times.map do |i|
123
+ all_rows.map { |r| r[i].to_s.length }.max
124
+ end
125
+
126
+ fmt = widths.map { |w| "%-#{w}s" }.join(' | ')
127
+ sep = widths.map { |w| '-' * w }.join('-|-')
128
+
129
+ puts "| #{format(fmt, *headers)} |"
130
+ puts "|-#{sep}-|"
131
+ rows.each { |r| puts "| #{format(fmt, *r)} |" }
132
+ end
133
+
134
+ def validate_sort!(sort)
135
+ return if sort.nil? || SORT_FIELDS.include?(sort)
136
+
137
+ warn "Error: Unsupported sort field '#{sort}'. Valid values: #{SORT_FIELDS.join(', ')}."
138
+ exit 1
139
+ end
140
+
141
+ def sort_rows(rows, sort)
142
+ return rows if sort.nil?
143
+
144
+ index = SORT_FIELDS.index(sort)
145
+ rows.sort_by { |row| [row[index].to_s.downcase, row[0].to_s.downcase] }
146
+ end
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Tree rendering
150
+ # ---------------------------------------------------------------------------
151
+
152
+ def value_label(value)
153
+ case value
154
+ when Hash then "[#{value.size}]"
155
+ when Array then "[#{value.size}]"
156
+ when Proc then '(lambda)'
157
+ else value.to_s
158
+ end
159
+ end
160
+
161
+ def render_connection_tree(conn, prefix)
162
+ keys = conn.keys
163
+ keys.each_with_index do |key, idx|
164
+ last = idx == keys.size - 1
165
+ connector = last ? TREE_LAST : TREE_FORK
166
+ child_prefix = prefix + (last ? TREE_BLANK : TREE_PIPE)
167
+ value = conn[key]
168
+
169
+ if value.is_a?(Hash)
170
+ puts "#{prefix}#{connector}#{key}"
171
+ sub_keys = value.keys
172
+ sub_keys.each_with_index do |sk, si|
173
+ sl = si == sub_keys.size - 1
174
+ sc = sl ? TREE_LAST : TREE_FORK
175
+ puts "#{child_prefix}#{sc}#{sk}"
176
+ end
177
+ else
178
+ puts "#{prefix}#{connector}#{key} #{value_label(value)}"
179
+ end
180
+ end
181
+ end
182
+
183
+ def render_children_tree(value, prefix)
184
+ child_keys = value.keys.sort
185
+ child_keys.each_with_index do |ck, ci|
186
+ cl = ci == child_keys.size - 1
187
+ puts "#{prefix}#{cl ? TREE_LAST : TREE_FORK}#{ck}"
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end