rubycli 0.1.7 → 0.2.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.
@@ -1,4 +1,6 @@
1
- require_relative "type_utils"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type_utils'
2
4
 
3
5
  module Rubycli
4
6
  class HelpRenderer
@@ -8,43 +10,42 @@ module Rubycli
8
10
  @documentation_registry = documentation_registry
9
11
  end
10
12
 
11
- def print_help(target, catalog)
12
- puts "Usage: #{File.basename($PROGRAM_NAME)} COMMAND [arguments]"
13
- puts
13
+ # +io+ is $stderr when the help is shown because a command failed, so that
14
+ # piping stdout to another program never mixes diagnostics into the payload.
15
+ def print_help(target, catalog, io: $stdout)
16
+ io.puts(help_text(target, catalog))
17
+ end
18
+
19
+ def help_text(_target, catalog)
20
+ lines = ["Usage: #{File.basename($PROGRAM_NAME)} COMMAND [arguments]", '']
14
21
 
15
22
  instance_entries = catalog.entries_for(:instance)
16
23
  class_entries = catalog.entries_for(:class)
17
24
  groups = []
18
- groups << { label: "Instance methods", entries: instance_entries } unless instance_entries.empty?
19
- groups << { label: "Class methods", entries: class_entries } unless class_entries.empty?
25
+ groups << { label: 'Instance methods', entries: instance_entries } unless instance_entries.empty?
26
+ groups << { label: 'Class methods', entries: class_entries } unless class_entries.empty?
20
27
 
21
- if groups.empty?
22
- puts "No commands available."
23
- return
24
- end
28
+ return (lines << 'No commands available.').join("\n") if groups.empty?
25
29
 
26
- puts "Available commands:"
30
+ lines << 'Available commands:'
27
31
  groups.each do |group|
28
- puts " #{group[:label]}:"
32
+ lines << " #{group[:label]}:"
29
33
  group[:entries].each do |entry|
30
34
  description = method_description(entry.method)
31
35
  line = " #{entry.command.ljust(20)}"
32
36
  line += " #{description}" unless description.empty?
33
- puts line.rstrip
37
+ lines << line.rstrip
34
38
 
35
- unless entry.aliases.empty?
36
- puts " Aliases: #{entry.aliases.join(", ")}"
37
- end
39
+ lines << " Aliases: #{entry.aliases.join(', ')}" unless entry.aliases.empty?
38
40
  end
39
- puts unless group.equal?(groups.last)
41
+ lines << '' unless group.equal?(groups.last)
40
42
  end
41
43
 
42
- if catalog.duplicates.any?
43
- puts "Methods with the same name can be invoked via instance::NAME / class::NAME."
44
- end
44
+ lines << 'Methods with the same name can be invoked via instance::NAME / class::NAME.' if catalog.duplicates.any?
45
45
 
46
- puts
47
- puts "Detailed command help: #{File.basename($PROGRAM_NAME)} COMMAND help"
46
+ lines << ''
47
+ lines << "Detailed command help: #{File.basename($PROGRAM_NAME)} COMMAND help"
48
+ lines.join("\n")
48
49
  end
49
50
 
50
51
  def method_description(method_obj)
@@ -53,7 +54,7 @@ module Rubycli
53
54
  return summary if summary && !summary.empty?
54
55
 
55
56
  params_str = format_method_parameters(method_obj.parameters, metadata)
56
- params_str.empty? ? "(no arguments)" : params_str
57
+ params_str.empty? ? '(no arguments)' : params_str
57
58
  end
58
59
 
59
60
  def usage_for_method(command, method)
@@ -69,17 +70,17 @@ module Rubycli
69
70
 
70
71
  returns = metadata[:returns] || []
71
72
  if returns.any?
72
- usage_lines << "" unless usage_lines.last == ""
73
- usage_lines << "Return values:"
73
+ usage_lines << '' unless usage_lines.last == ''
74
+ usage_lines << 'Return values:'
74
75
  returns.each do |ret|
75
- type_label = (ret.types || []).join(" | ")
76
+ type_label = (ret.types || []).join(' | ')
76
77
  line = " #{type_label}"
77
78
  line += " #{ret.description}" if ret.description && !ret.description.empty?
78
79
  usage_lines << line
79
80
  end
80
81
  end
81
82
 
82
- usage_lines.pop while usage_lines.last == ""
83
+ usage_lines.pop while usage_lines.last == ''
83
84
  usage_block = usage_lines.join("\n")
84
85
 
85
86
  sections = []
@@ -100,7 +101,7 @@ module Rubycli
100
101
  option_map = (metadata[:options] || []).each_with_object({}) { |opt, h| h[opt[:keyword]] = opt }
101
102
  positional_map = metadata[:positionals_map] || {}
102
103
 
103
- parameters.map { |type, name|
104
+ parameters.map do |type, name|
104
105
  case type
105
106
  when :req
106
107
  positional_usage_token(type, name, positional_map[name])
@@ -122,19 +123,19 @@ module Rubycli
122
123
  when :key
123
124
  opt = option_map[name]
124
125
  label = if opt
125
- if opt.doc_format == :auto_generated
126
- auto_generated_option_usage_label(name, opt)
127
- else
128
- option_flag_with_placeholder(opt)
129
- end
130
- else
131
- "--#{name.to_s.tr('_', '-')}=<value>"
132
- end
126
+ if opt.doc_format == :auto_generated
127
+ auto_generated_option_usage_label(name, opt)
128
+ else
129
+ option_flag_with_placeholder(opt)
130
+ end
131
+ else
132
+ "--#{name.to_s.tr('_', '-')}=<value>"
133
+ end
133
134
  "[#{label}]"
134
- when :keyrest then "[--<option>...]"
135
- else ""
135
+ when :keyrest then '[--<option>...]'
136
+ else ''
136
137
  end
137
- }.compact.reject(&:empty?).join(" ")
138
+ end.compact.reject(&:empty?).join(' ')
138
139
  end
139
140
 
140
141
  def auto_generated_option_usage_label(name, opt)
@@ -145,13 +146,13 @@ module Rubycli
145
146
  formatted = if value_name && !value_name.to_s.strip.empty?
146
147
  ensure_angle_bracket_placeholder(value_name)
147
148
  else
148
- "<value>"
149
+ '<value>'
149
150
  end
150
151
  "#{base_flag}=#{formatted}"
151
152
  end
152
153
 
153
154
  def render_positionals(positionals_in_order)
154
- rows = positionals_in_order.map do |info|
155
+ rows = positionals_in_order.map do |info|
155
156
  definition = info[:definition]
156
157
  label = info[:label]
157
158
  type = type_display(definition)
@@ -162,7 +163,7 @@ module Rubycli
162
163
  description_parts << default_text if default_text
163
164
  [label, type, requirement, description_parts.join(' ')]
164
165
  end
165
- table_block("Positional arguments:", rows)
166
+ table_block('Positional arguments:', rows)
166
167
  end
167
168
 
168
169
  def render_options(options, required_keywords)
@@ -176,7 +177,7 @@ module Rubycli
176
177
  description_parts << default_text if default_text
177
178
  [label, type, requirement, description_parts.join(' ').strip]
178
179
  end
179
- table_block("Options:", rows)
180
+ table_block('Options:', rows)
180
181
  end
181
182
 
182
183
  def table_block(header, rows)
@@ -185,7 +186,7 @@ module Rubycli
185
186
  cols = rows.transpose
186
187
  widths = cols.map { |col| col.map { |value| value.to_s.length }.max || 0 }
187
188
 
188
- lines = ["", header]
189
+ lines = ['', header]
189
190
  rows.each do |row|
190
191
  padded = row.each_with_index.map do |value, idx|
191
192
  text = value.to_s
@@ -247,32 +248,33 @@ module Rubycli
247
248
  end
248
249
 
249
250
  def positional_requirement(kind)
250
- kind == :opt ? 'optional' : 'required'
251
+ %i[opt rest].include?(kind) ? 'optional' : 'required'
251
252
  end
252
253
 
253
254
  def positional_default(definition)
254
255
  return nil unless definition
256
+
255
257
  value = definition.default_value
256
258
  return nil if value.nil? || value.to_s.empty?
257
259
 
258
260
  "(default: #{value})"
259
261
  end
260
262
 
261
- def option_default(opt)
262
- value = opt.default_value
263
- return nil if value.nil? || value.to_s.empty?
263
+ def option_default(opt)
264
+ value = opt.default_value
265
+ return nil if value.nil? || value.to_s.empty?
264
266
 
265
- "(default: #{value})"
266
- end
267
+ "(default: #{value})"
268
+ end
267
269
 
268
- def required_keyword_names(method)
269
- method.parameters.select { |type, _| type == :keyreq }.map { |_, name| name }
270
- end
270
+ def required_keyword_names(method)
271
+ method.parameters.select { |type, _| type == :keyreq }.map { |_, name| name }
272
+ end
271
273
 
272
274
  def ordered_positionals(method, metadata)
273
275
  positional_map = metadata[:positionals_map] || {}
274
276
  method.parameters.each_with_object([]) do |(type, name), memo|
275
- next unless %i[req opt].include?(type)
277
+ next unless %i[req opt rest].include?(type)
276
278
 
277
279
  definition = positional_map[name]
278
280
  label = display_label_for(definition, name)
@@ -296,8 +298,6 @@ module Rubycli
296
298
  optional_placeholder(placeholder, definition, name)
297
299
  when :rest
298
300
  rest_placeholder(placeholder, definition, name)
299
- else
300
- nil
301
301
  end
302
302
  end
303
303
 
@@ -319,7 +319,10 @@ module Rubycli
319
319
 
320
320
  def optional_placeholder(placeholder, definition, name)
321
321
  unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
322
- return placeholder.strip
322
+ documented = placeholder.strip
323
+ return documented if documented.start_with?('[') && documented.end_with?(']')
324
+
325
+ return "[#{documented}]"
323
326
  end
324
327
 
325
328
  "[#{default_positional_label(definition, name, uppercase: true)}]"
@@ -327,7 +330,10 @@ module Rubycli
327
330
 
328
331
  def rest_placeholder(placeholder, definition, name)
329
332
  unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
330
- return placeholder.strip
333
+ documented = placeholder.strip
334
+ documented = documented[1..-2].strip if documented.start_with?('[') && documented.end_with?(']')
335
+ documented = "#{documented}..." unless documented.end_with?('...')
336
+ return "[#{documented}]"
331
337
  end
332
338
 
333
339
  base = default_positional_label(definition, name, uppercase: true)
@@ -344,8 +350,8 @@ module Rubycli
344
350
 
345
351
  def option_flag_with_placeholder(opt)
346
352
  flags = [opt.short, opt.long].compact
347
- flags = ["--#{opt.keyword.to_s.tr("_", "-")}"] if flags.empty?
348
- flag_label = flags.join(", ")
353
+ flags = ["--#{opt.keyword.to_s.tr('_', '-')}"] if flags.empty?
354
+ flag_label = flags.join(', ')
349
355
  placeholder = option_value_placeholder(opt)
350
356
  if placeholder
351
357
  formatted = ensure_angle_bracket_placeholder(placeholder)
@@ -364,8 +370,7 @@ module Rubycli
364
370
  return nil if opt.boolean_flag
365
371
  return opt.value_name if opt.value_name && !opt.value_name.empty?
366
372
 
367
- first_non_nil_type = opt.types&.find { |type| !nil_type?(type) && !boolean_type?(type) }
368
- first_non_nil_type
373
+ opt.types&.find { |type| !nil_type?(type) && !boolean_type?(type) }
369
374
  end
370
375
 
371
376
  def auto_generated_placeholder?(placeholder, definition, name)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rubycli
2
4
  class JsonCoercer
3
5
  THREAD_KEY = :rubycli_json_mode
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rubycli
2
4
  class ResultEmitter
3
5
  def initialize(environment:)
@@ -26,15 +28,23 @@ module Rubycli
26
28
  when Array, Hash
27
29
  JSON.pretty_generate(result)
28
30
  else
29
- if result.respond_to?(:to_h)
30
- JSON.pretty_generate(result.to_h)
31
- elsif result.respond_to?(:to_ary)
32
- JSON.pretty_generate(result.to_ary)
33
- else
34
- result.inspect
35
- end
31
+ format_convertible_result(result)
32
+ end
33
+ rescue JSON::GeneratorError, JSON::NestingError
34
+ result.inspect
35
+ end
36
+
37
+ # to_h / to_ary are duck-typed: every Enumerable answers to_h, but Set,
38
+ # Range and Enumerator raise TypeError unless their elements are pairs.
39
+ def format_convertible_result(result)
40
+ if result.respond_to?(:to_h)
41
+ JSON.pretty_generate(result.to_h)
42
+ elsif result.respond_to?(:to_ary)
43
+ JSON.pretty_generate(result.to_ary)
44
+ else
45
+ result.inspect
36
46
  end
37
- rescue JSON::GeneratorError
47
+ rescue TypeError
38
48
  result.inspect
39
49
  end
40
50
  end