tty-prompt 0.21.0 → 0.22.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.
@@ -25,7 +25,7 @@ module TTY
25
25
  # Create answer entry
26
26
  #
27
27
  # @example
28
- # key(:name).ask('Name?')
28
+ # key(:name).ask("Name?")
29
29
  #
30
30
  # @api public
31
31
  def key(name, &block)
@@ -40,7 +40,7 @@ module TTY
40
40
  # Change to collect all values for a key
41
41
  #
42
42
  # @example
43
- # key(:colors).values.ask('Color?')
43
+ # key(:colors).values.ask("Color?")
44
44
  #
45
45
  # @api public
46
46
  def values(&block)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'paginator'
3
+ require_relative "paginator"
4
4
 
5
5
  module TTY
6
6
  class Prompt
@@ -15,7 +15,7 @@ module TTY
15
15
  # Choice.from([:foo, 1])
16
16
  # # => <TTY::Prompt::Choice @key=nil @name="foo" @value=1 @disabled=false>
17
17
  #
18
- # Choice.from({name: :foo, value: 1, key: 'f'}
18
+ # Choice.from({name: :foo, value: 1, key: "f"}
19
19
  # # => <TTY::Prompt::Choice @key="f" @name="foo" @value=1 @disabled=false>
20
20
  #
21
21
  # @param [Object] val
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
3
+ require "forwardable"
4
4
 
5
- require_relative 'choice'
5
+ require_relative "choice"
6
6
 
7
7
  module TTY
8
8
  class Prompt
@@ -13,15 +13,8 @@ module TTY
13
13
  include Enumerable
14
14
  extend Forwardable
15
15
 
16
- # The actual collection choices
17
- #
18
- # @return [Array[Choice]]
19
- #
20
- # @api public
21
- attr_reader :choices
22
-
23
16
  def_delegators :choices, :length, :size, :to_ary, :empty?,
24
- :values_at, :index
17
+ :values_at, :index, :==
25
18
 
26
19
  # Convenience for creating choices
27
20
  #
@@ -48,6 +41,20 @@ module TTY
48
41
  end
49
42
  end
50
43
 
44
+ # Scope of choices which are not disabled
45
+ #
46
+ # @api public
47
+ def enabled
48
+ reject(&:disabled?)
49
+ end
50
+
51
+ def enabled_indexes
52
+ each_with_index.reduce([]) do |acc, (choice, idx)|
53
+ acc << idx unless choice.disabled?
54
+ acc
55
+ end
56
+ end
57
+
51
58
  # Iterate over all choices in the collection
52
59
  #
53
60
  # @yield [Choice]
@@ -94,7 +101,7 @@ module TTY
94
101
  # Find a matching choice
95
102
  #
96
103
  # @exmaple
97
- # choices.find_by(:name, 'small')
104
+ # choices.find_by(:name, "small")
98
105
  #
99
106
  # @param [Symbol] attr
100
107
  # the attribute name
@@ -106,6 +113,16 @@ module TTY
106
113
  def find_by(attr, value)
107
114
  find { |choice| choice.public_send(attr) == value }
108
115
  end
116
+
117
+ protected
118
+
119
+ # The actual collection choices
120
+ #
121
+ # @return [Array[Choice]]
122
+ #
123
+ # @api private
124
+
125
+ attr_reader :choices
109
126
  end # Choices
110
127
  end # Prompt
111
128
  end # TTY
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'question'
4
- require_relative 'utils'
3
+ require_relative "question"
4
+ require_relative "utils"
5
5
 
6
6
  module TTY
7
7
  class Prompt
@@ -73,9 +73,9 @@ module TTY
73
73
  def render_question
74
74
  header = "#{@prefix}#{message} "
75
75
  if !@done
76
- header += @prompt.decorate("(#{@suffix})", @help_color) + ' '
76
+ header += @prompt.decorate("(#{@suffix})", @help_color) + " "
77
77
  else
78
- answer = convert_result(@input)
78
+ answer = conversion.call(@input)
79
79
  label = answer ? @positive : @negative
80
80
  header += @prompt.decorate(label, @active_color)
81
81
  end
@@ -98,11 +98,12 @@ module TTY
98
98
 
99
99
  # @api private
100
100
  def setup_defaults
101
+ infer_default
101
102
  @convert = conversion
102
103
  return if suffix? && positive?
103
104
 
104
105
  if suffix? && (!positive? || !negative?)
105
- parts = @suffix.split('/')
106
+ parts = @suffix.split("/")
106
107
  @positive = parts[0]
107
108
  @negative = parts[1]
108
109
  elsif !suffix? && positive?
@@ -112,18 +113,28 @@ module TTY
112
113
  end
113
114
  end
114
115
 
116
+ # @api private
117
+ def infer_default
118
+ converted = Converters.convert(:bool, default.to_s)
119
+ if converted == Const::Undefined
120
+ raise InvalidArgument, "default needs to be `true` or `false`"
121
+ else
122
+ default(converted)
123
+ end
124
+ end
125
+
115
126
  # @api private
116
127
  def create_default_labels
117
- @suffix = default ? 'Y/n' : 'y/N'
118
- @positive = default ? 'Yes' : 'yes'
119
- @negative = default ? 'no' : 'No'
128
+ @suffix = default ? "Y/n" : "y/N"
129
+ @positive = default ? "Yes" : "yes"
130
+ @negative = default ? "no" : "No"
120
131
  @validation = /^(y(es)?|no?)$/i
121
132
  @messages[:valid?] = "Invalid input."
122
133
  end
123
134
 
124
135
  # @api private
125
136
  def create_suffix
126
- (default ? positive.capitalize : positive.downcase) + '/' +
137
+ (default ? positive.capitalize : positive.downcase) + "/" +
127
138
  (default ? negative.downcase : negative.capitalize)
128
139
  end
129
140
 
@@ -131,12 +142,12 @@ module TTY
131
142
  #
132
143
  # @api private
133
144
  def conversion
134
- proc { |input|
145
+ ->(input) do
135
146
  positive_word = Regexp.escape(positive)
136
147
  positive_letter = Regexp.escape(positive[0])
137
- pattern = Regexp.new("^#{positive_word}|#{positive_letter}$", true)
148
+ pattern = Regexp.new("^(#{positive_word}|#{positive_letter})$", true)
138
149
  !input.match(pattern).nil?
139
- }
150
+ end
140
151
  end
141
152
  end # ConfirmQuestion
142
153
  end # Prompt
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Prompt
5
+ module Const
6
+ Undefined = Object.new.tap do |obj|
7
+ def obj.to_s
8
+ "undefined"
9
+ end
10
+
11
+ def obj.inspect
12
+ "undefined".inspect
13
+ end
14
+ end
15
+ end # Const
16
+ end # Prompt
17
+ end # TTY
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'converter_registry'
3
+ require_relative "converter_registry"
4
4
 
5
5
  module TTY
6
6
  class Prompt
7
7
  module ConverterDSL
8
8
  def converter_registry
9
- @converter_registry ||= ConverterRegistry.new
9
+ @__converter_registry ||= ConverterRegistry.new
10
10
  end
11
11
 
12
- def converter(name, &block)
13
- @converter_registry = converter_registry.register(name, &block)
14
- self
12
+ def converter(*names, &block)
13
+ converter_registry.register(*names, &block)
15
14
  end
16
15
 
17
- def convert(name, data)
18
- @converter_registry[name, data]
16
+ def convert(name, input)
17
+ converter_registry[name].call(input)
19
18
  end
20
19
  end # ConverterDSL
21
20
  end # Prompt
@@ -1,60 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module TTY
4
6
  class Prompt
5
7
  # Immutable collection of converters for type transformation
6
8
  #
7
9
  # @api private
8
10
  class ConverterRegistry
11
+ extend Forwardable
12
+
13
+ def_delegators "@__registry", :keys
14
+
9
15
  # Create a registry of conversions
10
16
  #
11
17
  # @param [Hash] registry
12
18
  #
13
19
  # @api private
14
20
  def initialize(registry = {})
15
- @_registry = registry.dup.freeze
16
- freeze
21
+ @__registry = registry.dup
17
22
  end
18
23
 
19
- # Register converter
24
+ # Check if conversion is available
20
25
  #
21
- # @param [Symbol] name
22
- # the converter name
26
+ # @param [String] name
27
+ #
28
+ # @return [Boolean]
23
29
  #
24
30
  # @api public
25
- def register(name, contents = nil, &block)
26
- item = block_given? ? block : contents
27
-
28
- if key?(name)
29
- raise ArgumentError,
30
- "Converter for #{name.inspect} already registered"
31
- end
32
- self.class.new(@_registry.merge(name => item))
31
+ def contain?(name)
32
+ conv_name = name.to_s.downcase.to_sym
33
+ @__registry.key?(conv_name)
33
34
  end
34
35
 
35
- # Check if converter is registered
36
+ # Register a conversion
36
37
  #
37
- # @return [Boolean]
38
+ # @param [Symbol] name
39
+ # the converter name
38
40
  #
39
41
  # @api public
40
- def key?(key)
41
- @_registry.key?(key)
42
+ def register(*names, &block)
43
+ names.each do |name|
44
+ if contain?(name)
45
+ raise ConversionAlreadyDefined,
46
+ "converter for #{name.inspect} is already registered"
47
+ end
48
+ @__registry[name] = block
49
+ end
42
50
  end
43
51
 
44
52
  # Execute converter
45
53
  #
46
54
  # @api public
47
- def call(name, input)
48
- if name.respond_to?(:call)
49
- converter = name
50
- else
51
- converter = @_registry.fetch(name) do
52
- raise ArgumentError, "#{name.inspect} is not registered"
53
- end
55
+ def [](name)
56
+ conv_name = name.to_s.downcase.to_sym
57
+ @__registry.fetch(conv_name) do
58
+ raise UnsupportedConversion,
59
+ "converter #{conv_name.inspect} is not registered"
54
60
  end
55
- converter[input]
56
61
  end
57
- alias [] call
62
+ alias fetch []
58
63
 
59
64
  def inspect
60
65
  @_registry.inspect
@@ -1,74 +1,181 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
4
- require 'necromancer'
5
-
6
- require_relative 'converter_dsl'
3
+ require_relative "const"
4
+ require_relative "converter_dsl"
7
5
 
8
6
  module TTY
9
7
  class Prompt
10
8
  module Converters
11
9
  extend ConverterDSL
12
10
 
13
- # Delegate Necromancer errors
14
- #
15
- # @api private
16
- def self.on_error
17
- if block_given?
18
- yield
19
- else
20
- raise ArgumentError, 'You need to provide a block argument.'
11
+ TRUE_VALUES = /^(t(rue)?|y(es)?|on|1)$/i.freeze
12
+ FALSE_VALUES = /^(f(alse)?|n(o)?|off|0)$/i.freeze
13
+
14
+ SINGLE_DIGIT_MATCHER = /^(?<digit>\-?\d+(\.\d+)?)$/.freeze
15
+ DIGIT_MATCHER = /^(?<open>-?\d+(\.\d+)?)
16
+ \s*(?<sep>(\.\s*){2,3}|-|,)\s*
17
+ (?<close>-?\d+(\.\d+)?)$
18
+ /x.freeze
19
+ LETTER_MATCHER = /^(?<open>\w)
20
+ \s*(?<sep>(\.\s*){2,3}|-|,)\s*
21
+ (?<close>\w)$
22
+ /x.freeze
23
+
24
+ converter(:boolean, :bool) do |input|
25
+ case input.to_s
26
+ when TRUE_VALUES then true
27
+ when FALSE_VALUES then false
28
+ else Const::Undefined
21
29
  end
22
- rescue Necromancer::ConversionTypeError => e
23
- raise ConversionError, e.message
24
- end
25
-
26
- converter(:bool) do |input|
27
- on_error { Necromancer.convert(input).to(:boolean, strict: true) }
28
30
  end
29
31
 
30
- converter(:string) do |input|
32
+ converter(:string, :str) do |input|
31
33
  String(input).chomp
32
34
  end
33
35
 
34
- converter(:symbol) do |input|
36
+ converter(:symbol, :sym) do |input|
35
37
  input.to_sym
36
38
  end
37
39
 
40
+ converter(:char) do |input|
41
+ String(input).chars.to_a[0]
42
+ end
43
+
38
44
  converter(:date) do |input|
39
- on_error { Necromancer.convert(input).to(:date, strict: true) }
45
+ begin
46
+ require "date" unless defined?(::Date)
47
+ ::Date.parse(input)
48
+ rescue ArgumentError
49
+ Const::Undefined
50
+ end
40
51
  end
41
52
 
42
53
  converter(:datetime) do |input|
43
- on_error { Necromancer.convert(input).to(:datetime, strict: true) }
54
+ begin
55
+ require "date" unless defined?(::Date)
56
+ ::DateTime.parse(input.to_s)
57
+ rescue ArgumentError
58
+ Const::Undefined
59
+ end
44
60
  end
45
61
 
46
- converter(:int) do |input|
47
- on_error { Necromancer.convert(input).to(:integer, strict: true) }
62
+ converter(:time) do |input|
63
+ begin
64
+ require "time"
65
+ ::Time.parse(input.to_s)
66
+ rescue ArgumentError
67
+ Const::Undefined
68
+ end
69
+ end
70
+
71
+ converter(:integer, :int) do |input|
72
+ begin
73
+ Integer(input)
74
+ rescue ArgumentError
75
+ Const::Undefined
76
+ end
48
77
  end
49
78
 
50
79
  converter(:float) do |input|
51
- on_error { Necromancer.convert(input).to(:float, strict: true) }
80
+ begin
81
+ Float(input)
82
+ rescue TypeError, ArgumentError
83
+ Const::Undefined
84
+ end
52
85
  end
53
86
 
87
+ # Convert string number to integer or float
88
+ #
89
+ # @return [Integer,Float,Const::Undefined]
90
+ #
91
+ # @api private
92
+ def cast_to_num(num)
93
+ ([convert(:int, num), convert(:float, num)] - [Const::Undefined]).first ||
94
+ Const::Undefined
95
+ end
96
+ module_function :cast_to_num
97
+
54
98
  converter(:range) do |input|
55
- on_error { Necromancer.convert(input).to(:range, strict: true) }
99
+ if input.is_a?(::Range)
100
+ input
101
+ elsif match = input.to_s.match(SINGLE_DIGIT_MATCHER)
102
+ digit = cast_to_num(match[:digit])
103
+ ::Range.new(digit, digit)
104
+ elsif match = input.to_s.match(DIGIT_MATCHER)
105
+ open = cast_to_num(match[:open])
106
+ close = cast_to_num(match[:close])
107
+ ::Range.new(open, close, match[:sep].gsub(/\s*/, "") == "...")
108
+ elsif match = input.to_s.match(LETTER_MATCHER)
109
+ ::Range.new(match[:open], match[:close],
110
+ match[:sep].gsub(/\s*/, "") == "...")
111
+ else Const::Undefined
112
+ end
56
113
  end
57
114
 
58
115
  converter(:regexp) do |input|
59
116
  Regexp.new(input)
60
117
  end
61
118
 
62
- converter(:file) do |input|
63
- ::File.open(::File.join(Dir.pwd, input))
119
+ converter(:filepath, :file) do |input|
120
+ ::File.expand_path(input)
64
121
  end
65
122
 
66
- converter(:path) do |input|
67
- Pathname.new(::File.join(Dir.pwd, input))
123
+ converter(:pathname, :path) do |input|
124
+ require "pathname" unless defined?(::Pathname)
125
+ ::Pathname.new(input)
68
126
  end
69
127
 
70
- converter(:char) do |input|
71
- String(input).chars.to_a[0]
128
+ converter(:uri) do |input|
129
+ require "uri" unless defined?(::URI)
130
+ ::URI.parse(input)
131
+ end
132
+
133
+ converter(:list, :array) do |val|
134
+ (val.respond_to?(:to_a) ? val : val.split(/(?<!\\),/))
135
+ .map { |v| v.strip.gsub(/\\,/, ",") }
136
+ .reject(&:empty?)
137
+ end
138
+
139
+ converter(:hash, :map) do |val|
140
+ values = val.respond_to?(:to_a) ? val : val.split(/[& ]/)
141
+ values.each_with_object({}) do |pair, pairs|
142
+ key, value = pair.split(/[=:]/, 2)
143
+ if (current = pairs[key.to_sym])
144
+ pairs[key.to_sym] = Array(current) << value
145
+ else
146
+ pairs[key.to_sym] = value
147
+ end
148
+ pairs
149
+ end
150
+ end
151
+
152
+ converter_registry.keys.each do |type|
153
+ next if type =~ /list|array|map|hash/
154
+
155
+ [:"#{type}_list", :"#{type}_array", :"#{type}s"].each do |new_type|
156
+ converter(new_type) do |val|
157
+ converter_registry[:array].(val).map do |obj|
158
+ converter_registry[type].(obj)
159
+ end
160
+ end
161
+ end
162
+
163
+ [:"#{type}_map", :"#{type}_hash"].each do |new_type|
164
+ converter(new_type) do |val|
165
+ converter_registry[:hash].(val).each_with_object({}) do |(k, v), h|
166
+ h[k] = converter_registry[type].(v)
167
+ end
168
+ end
169
+ end
170
+
171
+ [:"string_#{type}_map", :"str_#{type}_map",
172
+ :"string_#{type}_hash", :"str_#{type}_hash"].each do |new_type|
173
+ converter(new_type) do |val|
174
+ converter_registry[:hash].(val).each_with_object({}) do |(k, v), h|
175
+ h[converter_registry[:string].(k)] = converter_registry[type].(v)
176
+ end
177
+ end
178
+ end
72
179
  end
73
180
  end # Converters
74
181
  end # Prompt