tty-prompt 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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