simple_input 0.1.1 → 0.2.2

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: d023f4e982fe0d178cb837be8de3962107c1a4a7f47569ee527cb7a650254f36
4
- data.tar.gz: 894b4a44bc81b8bd01eec6b8847dde883f1df115161ea7038e16ade3b04f17ba
3
+ metadata.gz: 067ce86ac266a279491b935536c473fe2b257e9a3cd3556877c7affe2be3311c
4
+ data.tar.gz: e0f078fa1ee462081ed0d2da0d76f44461b6e90d1d595cf1beea1186574937e6
5
5
  SHA512:
6
- metadata.gz: 4dbcd11a33cdfd07a39c139a629a2b2753810525dc6318a86a437f585bf3734a3a356511641b8232e23f678959e90c71c0b66bc1ecc3e61bf34685e81cc327da
7
- data.tar.gz: 0a47a918d2ee493e27e4f14d20b62c8d6b8989508475ecb6e086903aefe28d5bfeb219f85619c97215d9106afd3536ac52c6f630436c5b349596ca3fd649d8cf
6
+ metadata.gz: c488763914f58752e2f230e78df81e55bc88b53587218ddfeee91ac855c35ffb18a4c8260911615652c058803fd36acf2ee64a53840b704245af23d9a01da736
7
+ data.tar.gz: f165c355abc6a8776b38bdbc47f2b35991cee593c8ccc776304fca1cf60260186a8b361b7b559225cd2a080aebf31f4544c7eaf4e70f8f34146b23e2c8609391
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hiroaki Satou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,320 @@
1
+ # Simple Input
2
+
3
+ this is a simple prompt library for ruby.
4
+ inspired by golang's huh! library's input component.
5
+ https://github.com/charmbracelet/huh
6
+
7
+ ## Requirements
8
+
9
+ - Ruby 3.4+
10
+ - Bundler
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ git clone <this-repo>
16
+ cd simple-input
17
+ bundle install
18
+ ```
19
+
20
+ or
21
+
22
+ ```bash
23
+ gem install simple-input
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Basic Input
29
+
30
+ ```ruby
31
+ require_relative 'input'
32
+
33
+ name = Input.new_input
34
+ .title('What is your name?')
35
+ .prompt('> ')
36
+ .validate(:not_empty)
37
+ .run
38
+
39
+ puts "Hello, #{name}!"
40
+ ```
41
+
42
+ ### Custom Prompt
43
+
44
+ ```ruby
45
+ email = Input.new_input
46
+ .title('Enter your email')
47
+ .prompt('email> ')
48
+ .validate { |val| val.include?('@') ? nil : 'Please enter a valid email' }
49
+ .run
50
+ ```
51
+
52
+ ### Multiple Validators
53
+
54
+ Validators run in order. The first error found is displayed.
55
+
56
+ ```ruby
57
+ password = Input.new_input
58
+ .title('Create a password')
59
+ .prompt('password> ')
60
+ .validate(:not_empty)
61
+ .validate { |val| val.length < 8 ? 'Must be at least 8 characters' : nil }
62
+ .run
63
+ ```
64
+
65
+ ### Custom Validator Provider
66
+
67
+ By default, symbol validators (e.g., `:not_empty`) resolve against `DefaultValidators`. You can swap in your own module with `with_validators`:
68
+
69
+ ```ruby
70
+ module MyValidators
71
+ module_function
72
+
73
+ def email(val)
74
+ val.match?(/\A[^@\s]+@[^@\s]+\z/) ? nil : 'Invalid email format'
75
+ end
76
+
77
+ def min_length(val)
78
+ val.length >= 3 ? nil : 'Must be at least 3 characters'
79
+ end
80
+ end
81
+
82
+ username = Input.new_input
83
+ .with_validators(MyValidators)
84
+ .validate(:min_length)
85
+ .run
86
+ ```
87
+
88
+ ### Validation Types
89
+
90
+ You can pass validators in three ways:
91
+
92
+ | Type | Example |
93
+ |------|---------|
94
+ | **Symbol** | `.validate(:not_empty)` - looks up method on the validator provider |
95
+ | **Proc/Lambda** | `.validate(-> (val) { val.empty? ? 'required' : nil })` |
96
+ | **Block** | `.validate { \|val\| val.empty? ? 'required' : nil }` |
97
+
98
+ Validators are functions that take a `String` and return `nil` (valid) or an error message `String`.
99
+
100
+ ### Value Conversion
101
+
102
+ Use `convert_func` to transform the input value before it's returned. This is useful for converting strings to integers, floats, arrays, or any other type.
103
+
104
+ ```ruby
105
+ # Convert to integer
106
+ age = Input.new_input
107
+ .title('How old are you?')
108
+ .validate(:not_empty)
109
+ .validate { |val| val.match?(/^\d+$/) ? nil : 'Must be a number' }
110
+ .convert_func { |val| val.to_i }
111
+ .run
112
+
113
+ puts age.class # => Integer
114
+ ```
115
+
116
+ ```ruby
117
+ # Convert to array
118
+ tags = Input.new_input
119
+ .title('Enter tags (comma-separated)')
120
+ .convert_func { |val| val.split(',').map(&:strip) }
121
+ .run
122
+
123
+ puts tags.inspect # => ["ruby", "cli", "prompt"]
124
+ ```
125
+
126
+ You can pass a converter in two ways:
127
+
128
+ | Type | Example |
129
+ |------|---------|
130
+ | **Proc/Lambda** | `.convert_func(-> (val) { val.to_i })` |
131
+ | **Block** | `.convert_func { \|val\| val.to_i }` |
132
+
133
+ Converters are functions that take a `String` and return any type. Validation happens before conversion.
134
+
135
+ **Important:** If conversion fails with an exception, an error message will be displayed prompting you to add validation. Always validate input before conversion to prevent errors:
136
+
137
+ ```ruby
138
+ # ❌ Bad - conversion can fail without validation
139
+ age = Input.new_input
140
+ .convert_func { |val| Integer(val) } # Raises exception on invalid input
141
+ .run
142
+
143
+ # ✅ Good - validate before conversion
144
+ age = Input.new_input
145
+ .validate(:not_empty)
146
+ .validate { |val| val.match?(/^\d+$/) ? nil : 'Must be a number' }
147
+ .convert_func { |val| val.to_i }
148
+ .run
149
+ ```
150
+
151
+ ### Signal Handling
152
+
153
+ - **Ctrl-C** - Prints `(canceled)` and exits gracefully
154
+ - **Ctrl-D (EOF)** - Returns an empty string
155
+
156
+ ## API
157
+
158
+ | Method | Description |
159
+ |--------|-------------|
160
+ | `Input.new_input` | Factory method to create a new Input (`.new` is private) |
161
+ | `#title(text)` | Set the title displayed above the prompt |
162
+ | `#prompt(text)` | Set the prompt string (default: `"> "`) |
163
+ | `#validate(validator, &block)` | Add a validator (Symbol, Proc, or block) |
164
+ | `#with_validators(provider)` | Set a custom validator provider module |
165
+ | `#convert_func(converter, &block)` | Set a converter function to transform the input value |
166
+ | `#run` | Start the interactive input loop and return the result |
167
+
168
+ All methods (except `#run`) return `self` for chaining.
169
+
170
+ ## Complete Example: Area Calculator
171
+
172
+ Here's a practical example that demonstrates validation, conversion, and dependency injection for testing:
173
+
174
+ ```ruby
175
+ class AreaCalculator
176
+ def initialize(reader = $stdin, writer = $stdout)
177
+ @reader = reader
178
+ @writer = writer
179
+ end
180
+
181
+ # Reusable conversion function
182
+ def to_number
183
+ ->(value) do
184
+ if value.to_i.to_s == value
185
+ value.to_i
186
+ elsif value.to_f.to_s == value
187
+ value.to_f
188
+ else
189
+ raise ArgumentError, "Invalid number: #{value}"
190
+ end
191
+ end
192
+ end
193
+
194
+ def input_length
195
+ Input.new_input
196
+ .send(:with_context, @reader, @writer)
197
+ .title('What is the length of the room in feet?')
198
+ .validate { |value| value.to_i > 0 ? nil : 'Length must be a positive number' }
199
+ .convert_func(to_number)
200
+ .run
201
+ end
202
+
203
+ def input_width
204
+ Input.new_input
205
+ .send(:with_context, @reader, @writer)
206
+ .title('What is the width of the room in feet?')
207
+ .validate { |value| value.to_i > 0 ? nil : 'Width must be a positive number' }
208
+ .convert_func(to_number)
209
+ .run
210
+ end
211
+
212
+ def calculate_area
213
+ length = input_length
214
+ width = input_width
215
+ area = length * width
216
+
217
+ @writer.puts "\nYou entered dimensions of #{length} feet by #{width} feet."
218
+ @writer.puts "The area is #{area} square feet"
219
+ end
220
+ end
221
+ ```
222
+
223
+ This example can be tested with RSpec using mocks:
224
+
225
+ ```ruby
226
+ RSpec.describe AreaCalculator do
227
+ let(:writer) { double('writer') }
228
+ let(:reader) { double('reader') }
229
+ subject(:calculator) { AreaCalculator.new(reader, writer) }
230
+
231
+ before do
232
+ allow(writer).to receive(:puts)
233
+ allow(writer).to receive(:print)
234
+ end
235
+
236
+ it 'returns integer for valid integer input' do
237
+ allow(reader).to receive(:gets).and_return("10\n")
238
+
239
+ result = calculator.input_length
240
+
241
+ expect(result).to eq(10)
242
+ end
243
+
244
+ it 'returns float for valid float input' do
245
+ allow(reader).to receive(:gets).and_return("10.5\n")
246
+
247
+ result = calculator.input_length
248
+
249
+ expect(result).to eq(10.5)
250
+ end
251
+ end
252
+ ```
253
+
254
+ See `examples/area_calculator.rb` and `examples/area_calculator_spec.rb` for the complete implementation.
255
+
256
+ ## Running Examples
257
+
258
+ ```bash
259
+ # Basic usage and validation
260
+ ruby examples/basic.rb
261
+
262
+ # Custom validator providers
263
+ ruby examples/custom_validators.rb
264
+
265
+ # Value conversion (string to int, array, etc.)
266
+ ruby examples/convert.rb
267
+
268
+ # Common validation patterns (email, password, range, choice)
269
+ ruby examples/validation_patterns.rb
270
+
271
+ # Error handling and conversion safety
272
+ ruby examples/error_handling.rb
273
+
274
+ # Area calculator with numeric conversion (practical example)
275
+ ruby examples/area_calculator.rb
276
+ ```
277
+
278
+ ## Testing
279
+
280
+ ```bash
281
+ bundle exec rspec
282
+ ```
283
+
284
+ ### Testing with `with_context` (Private Method)
285
+
286
+ The `Input` class has a private method `with_context(reader, writer)` that allows injecting custom IO objects for testing. Since `new` is also private (enforcing use of `Input.new_input`), tests use `send` to access `with_context`:
287
+
288
+ ```ruby
289
+ def build_input(input_text)
290
+ reader = StringIO.new(input_text)
291
+ writer = StringIO.new
292
+ input = Input.new_input
293
+ input.send(:with_context, reader, writer)
294
+ [input, writer]
295
+ end
296
+
297
+ # Usage in tests
298
+ it 'returns the user input' do
299
+ input, = build_input("hello\n")
300
+ result = input.run
301
+ expect(result).to eq('hello')
302
+ end
303
+
304
+ it 'shows validation errors' do
305
+ input, writer = build_input("\nhello\n")
306
+ result = input.validate(:not_empty).run
307
+ expect(result).to eq('hello')
308
+ expect(writer.string).to include('入力してください')
309
+ end
310
+ ```
311
+
312
+ This pattern lets you:
313
+ - Simulate user input via `StringIO` as the reader
314
+ - Capture all terminal output via `StringIO` as the writer
315
+ - Test the full input loop including validation re-prompting without interactive terminal input
316
+
317
+
318
+ ## License
319
+
320
+ MIT
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/simple_prompt'
5
+
6
+ # Example: Area Calculator with Input Validation and Conversion
7
+ # This example demonstrates using Input with:
8
+ # - Custom validation for positive numbers
9
+ # - Value conversion from string to integer/float
10
+ # - Reusable conversion function
11
+
12
+ class AreaCalculator
13
+ def initialize(reader = $stdin, writer = $stdout)
14
+ @reader = reader
15
+ @writer = writer
16
+ end
17
+
18
+ # Conversion function that converts string to appropriate numeric type
19
+ def to_number
20
+ ->(value) do
21
+ if value.to_i.to_s == value
22
+ value.to_i
23
+ elsif value.to_f.to_s == value
24
+ value.to_f
25
+ else
26
+ raise ArgumentError, "Invalid number: #{value}"
27
+ end
28
+ end
29
+ end
30
+
31
+ def input_length
32
+ Input.new_input
33
+ .send(:with_context, @reader, @writer)
34
+ .title('What is the length of the room in feet?')
35
+ .validate { |value| value.to_i > 0 ? nil : 'Length must be a positive number' }
36
+ .convert_func(to_number)
37
+ .run
38
+ end
39
+
40
+ def input_width
41
+ Input.new_input
42
+ .send(:with_context, @reader, @writer)
43
+ .title('What is the width of the room in feet?')
44
+ .validate { |value| value.to_i > 0 ? nil : 'Width must be a positive number' }
45
+ .convert_func(to_number)
46
+ .run
47
+ end
48
+
49
+ def calculate_area
50
+ length = input_length
51
+ width = input_width
52
+ area = length * width
53
+
54
+ @writer.puts "\nYou entered dimensions of #{length} feet by #{width} feet."
55
+ @writer.puts "The area is"
56
+ @writer.puts "#{area} square feet"
57
+ @writer.puts "#{(area * 0.09290304).round(3)} square meters"
58
+ end
59
+ end
60
+
61
+ # Run the calculator if this file is executed directly
62
+ if __FILE__ == $PROGRAM_NAME
63
+ calculator = AreaCalculator.new
64
+ calculator.calculate_area
65
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/simple_prompt'
4
+ require_relative 'area_calculator'
5
+
6
+ # Example RSpec tests for area_calculator.rb
7
+ # This demonstrates how to test Input with mocks
8
+
9
+ RSpec.describe AreaCalculator do
10
+ let(:writer) { double('writer') }
11
+ let(:reader) { double('reader') }
12
+ subject(:calculator) { AreaCalculator.new(reader, writer) }
13
+
14
+ before do
15
+ allow(writer).to receive(:puts)
16
+ allow(writer).to receive(:print)
17
+ end
18
+
19
+ describe '#input_length' do
20
+ it 'returns integer for valid integer input' do
21
+ allow(reader).to receive(:gets).and_return("10\n")
22
+
23
+ result = calculator.input_length
24
+
25
+ expect(result).to eq(10)
26
+ end
27
+
28
+ it 'returns float for valid float input' do
29
+ allow(reader).to receive(:gets).and_return("10.5\n")
30
+
31
+ result = calculator.input_length
32
+
33
+ expect(result).to eq(10.5)
34
+ end
35
+ end
36
+
37
+ describe '#input_width' do
38
+ it 'returns integer for valid integer input' do
39
+ allow(reader).to receive(:gets).and_return("20\n")
40
+
41
+ result = calculator.input_width
42
+
43
+ expect(result).to eq(20)
44
+ end
45
+
46
+ it 'returns float for valid float input' do
47
+ allow(reader).to receive(:gets).and_return("20.3\n")
48
+
49
+ result = calculator.input_width
50
+
51
+ expect(result).to eq(20.3)
52
+ end
53
+ end
54
+ end
data/examples/basic.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../input'
4
+
5
+ # Basic usage - simple text input
6
+ name = Input.new_input
7
+ .title('What is your name?')
8
+ .prompt('> ')
9
+ .validate(:not_empty)
10
+ .run
11
+
12
+ puts "Hello, #{name}!"
13
+
14
+ # With custom validation
15
+ email = Input.new_input
16
+ .title('Enter your email')
17
+ .prompt('email> ')
18
+ .validate { |val| val.include?('@') ? nil : 'Please enter a valid email' }
19
+ .run
20
+
21
+ puts "Your email: #{email}"
22
+
23
+ # Multiple validators
24
+ password = Input.new_input
25
+ .title('Create a password')
26
+ .prompt('password> ')
27
+ .validate(:not_empty)
28
+ .validate { |val| val.length < 8 ? 'Password must be at least 8 characters' : nil }
29
+ .run
30
+
31
+ puts "Password set! (#{password.length} characters)"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../input'
4
+
5
+ # Convert string to integer with proper validation
6
+ # Validation prevents conversion errors
7
+ age = Input.new_input
8
+ .title('How old are you?')
9
+ .prompt('age> ')
10
+ .validate(:not_empty)
11
+ .validate { |val| val.match?(/^\d+$/) ? nil : 'Please enter a valid number' }
12
+ .convert_func { |val| val.to_i }
13
+ .run
14
+
15
+ puts "You are #{age} years old (type: #{age.class})"
16
+
17
+ # Example: Without validation, conversion can fail
18
+ # This will show an error message prompting you to add validation
19
+ puts "\n--- Example of conversion error (try entering 'abc') ---"
20
+ number = Input.new_input
21
+ .title('Enter a number (without validation)')
22
+ .prompt('> ')
23
+ .convert_func { |val| Integer(val) } # Integer() raises exception on invalid input
24
+ .run
25
+
26
+ puts "Number: #{number}"
27
+
28
+ # Convert to uppercase
29
+ name = Input.new_input
30
+ .title('What is your name?')
31
+ .prompt('name> ')
32
+ .validate(:not_empty)
33
+ .convert_func { |val| val.upcase }
34
+ .run
35
+
36
+ puts "Hello, #{name}!"
37
+
38
+ # Convert to array by splitting
39
+ tags = Input.new_input
40
+ .title('Enter tags (comma-separated)')
41
+ .prompt('tags> ')
42
+ .validate(:not_empty)
43
+ .convert_func { |val| val.split(',').map(&:strip) }
44
+ .run
45
+
46
+ puts "Tags: #{tags.inspect} (type: #{tags.class})"
47
+
48
+ # Using Proc instead of block
49
+ to_float = proc { |val| val.to_f }
50
+ price = Input.new_input
51
+ .title('Enter price')
52
+ .prompt('$')
53
+ .validate(:not_empty)
54
+ .validate { |val| val.match?(/^\d+\.?\d*$/) ? nil : 'Please enter a valid price' }
55
+ .convert_func(to_float)
56
+ .run
57
+
58
+ puts "Price: $#{price} (type: #{price.class})"
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../input'
4
+
5
+ # Define a custom validator provider
6
+ module MyValidators
7
+ module_function
8
+
9
+ def email(val)
10
+ val.match?(/\A[^@\s]+@[^@\s]+\z/) ? nil : 'Invalid email format'
11
+ end
12
+
13
+ def min_length(val)
14
+ val.length >= 3 ? nil : 'Must be at least 3 characters'
15
+ end
16
+ end
17
+
18
+ # Use with_validators to swap in your custom provider
19
+ username = Input.new_input
20
+ .title('Choose a username')
21
+ .prompt('>> ')
22
+ .with_validators(MyValidators)
23
+ .validate(:min_length)
24
+ .run
25
+
26
+ puts "Username: #{username}"
27
+
28
+ email = Input.new_input
29
+ .title('Enter your email')
30
+ .prompt('>> ')
31
+ .with_validators(MyValidators)
32
+ .validate(:email)
33
+ .run
34
+
35
+ puts "Email: #{email}"
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../input'
4
+
5
+ puts "=== Error Handling Example ===\n\n"
6
+
7
+ # Example 1: Conversion without validation (will show error on invalid input)
8
+ puts "Try entering 'abc' to see the error message:"
9
+ number = Input.new_input
10
+ .title('Enter a number (no validation)')
11
+ .prompt('> ')
12
+ .convert_func { |val| Integer(val) }
13
+ .run
14
+
15
+ puts "Number: #{number}\n\n"
16
+
17
+ # Example 2: Proper validation before conversion
18
+ puts "Now with validation:"
19
+ safe_number = Input.new_input
20
+ .title('Enter a number (with validation)')
21
+ .prompt('> ')
22
+ .validate(:not_empty)
23
+ .validate { |val| val.match?(/^-?\d+$/) ? nil : 'Must be a valid integer' }
24
+ .convert_func { |val| val.to_i }
25
+ .run
26
+
27
+ puts "Number: #{safe_number}\n\n"
28
+
29
+ # Example 3: Float conversion with validation
30
+ price = Input.new_input
31
+ .title('Enter price')
32
+ .prompt('$')
33
+ .validate(:not_empty)
34
+ .validate { |val| val.match?(/^\d+\.?\d*$/) ? nil : 'Must be a valid price (e.g., 10 or 10.99)' }
35
+ .convert_func { |val| val.to_f }
36
+ .run
37
+
38
+ puts "Price: $#{format('%.2f', price)}"
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../input'
4
+
5
+ puts "=== Validation Patterns Example ===\n\n"
6
+
7
+ # Pattern 1: Email validation
8
+ email = Input.new_input
9
+ .title('Enter your email address')
10
+ .prompt('email> ')
11
+ .validate(:not_empty)
12
+ .validate { |val| val.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/) ? nil : 'Invalid email format' }
13
+ .run
14
+
15
+ puts "Email registered: #{email}\n\n"
16
+
17
+ # Pattern 2: Password with multiple rules
18
+ password = Input.new_input
19
+ .title('Create a secure password')
20
+ .prompt('password> ')
21
+ .validate(:not_empty)
22
+ .validate { |val| val.length >= 8 ? nil : 'Must be at least 8 characters' }
23
+ .validate { |val| val.match?(/[A-Z]/) ? nil : 'Must contain at least one uppercase letter' }
24
+ .validate { |val| val.match?(/[0-9]/) ? nil : 'Must contain at least one number' }
25
+ .run
26
+
27
+ puts "Password created! (#{password.length} characters)\n\n"
28
+
29
+ # Pattern 3: Range validation with conversion
30
+ age = Input.new_input
31
+ .title('Enter your age')
32
+ .prompt('age> ')
33
+ .validate(:not_empty)
34
+ .validate { |val| val.match?(/^\d+$/) ? nil : 'Must be a number' }
35
+ .validate { |val| val.to_i.between?(1, 120) ? nil : 'Age must be between 1 and 120' }
36
+ .convert_func { |val| val.to_i }
37
+ .run
38
+
39
+ puts "Age: #{age} (type: #{age.class})\n\n"
40
+
41
+ # Pattern 4: Choice validation
42
+ choice = Input.new_input
43
+ .title('Select your preferred language')
44
+ .prompt('(ruby/python/javascript)> ')
45
+ .validate(:not_empty)
46
+ .validate { |val| %w[ruby python javascript].include?(val.downcase) ? nil : 'Must be ruby, python, or javascript' }
47
+ .convert_func { |val| val.downcase.to_sym }
48
+ .run
49
+
50
+ puts "Selected language: #{choice} (type: #{choice.class})"
@@ -10,6 +10,7 @@ class Input
10
10
 
11
11
  ValidatorProc = T.type_alias { T.proc.params(arg0: String).returns(T.nilable(String)) }
12
12
  ValidatorType = T.type_alias { T.any(Symbol, ValidatorProc) }
13
+ ConvertProc = T.type_alias { T.proc.params(arg0: String).returns(T.untyped) }
13
14
 
14
15
  sig { returns(Input) }
15
16
  def self.new_input; new; end
@@ -18,12 +19,13 @@ class Input
18
19
 
19
20
  sig { void }
20
21
  def initialize
21
- @reader = T.let($stdin, T.any(IO, StringIO))
22
- @writer = T.let($stdout, T.any(IO, StringIO))
22
+ @reader = T.let($stdin, T.untyped)
23
+ @writer = T.let($stdout, T.untyped)
23
24
  @title = T.let('', String)
24
25
  @prompt = T.let('> ', String)
25
26
  @validators = T.let([], T::Array[ValidatorProc])
26
27
  @validator_provider = T.let(DefaultValidators, T.untyped)
28
+ @convert_func = T.let(nil, T.nilable(ConvertProc))
27
29
  end
28
30
 
29
31
  sig { params(text: String).returns(T.self_type) }
@@ -35,6 +37,12 @@ class Input
35
37
  sig { params(provider: T.untyped).returns(T.self_type) }
36
38
  def with_validators(provider); @validator_provider = provider; self; end
37
39
 
40
+ sig { params(converter: T.nilable(ConvertProc), block: T.nilable(ConvertProc)).returns(T.self_type) }
41
+ def convert_func(converter = nil, &block)
42
+ @convert_func = block || converter
43
+ self
44
+ end
45
+
38
46
  sig { params(validator: T.nilable(ValidatorType), block: T.nilable(ValidatorProc)).returns(T.self_type) }
39
47
  def validate(validator = nil, &block)
40
48
  if block
@@ -51,7 +59,7 @@ class Input
51
59
  self
52
60
  end
53
61
 
54
- sig { returns(String) }
62
+ sig { returns(T.untyped) }
55
63
  def run
56
64
  loop do
57
65
  @writer.puts "\e[1m#{@title}\e[0m" unless @title.empty?
@@ -64,7 +72,13 @@ class Input
64
72
  error = T.let(nil, T.nilable(String))
65
73
  @validators.each { |v| break if (error = v.call(val)) }
66
74
 
67
- return val unless error
75
+ unless error
76
+ begin
77
+ return @convert_func ? @convert_func.call(val) : val
78
+ rescue StandardError => e
79
+ error = "Conversion error: #{e.message} (please add validation)"
80
+ end
81
+ end
68
82
 
69
83
  # 視認性のための余白
70
84
  @writer.puts ''
@@ -78,6 +92,6 @@ class Input
78
92
 
79
93
  private
80
94
 
81
- sig { params(r: T.any(IO, StringIO), w: T.any(IO, StringIO)).returns(T.self_type) }
82
- def with_context(r, w); @reader = r; @writer = w; self; end
95
+ sig { params(reader: T.untyped, writer: T.untyped).returns(T.self_type) }
96
+ def with_context(reader, writer); @reader = reader; @writer = writer; self; end
83
97
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimplePrompt
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.2'
5
5
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_input
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroaki Satou
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -93,16 +93,24 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: 0.5.100
96
- description: This is a simple prompt library for ruby. Inspired by golang's huh! library's
97
- input component (https://github.com/charmbracelet/huh). If somebody needs it, I
98
- would make select, multi-select, confirm, text library in addition with AI. But
99
- my use case I need input only, so I simply AI tested it only. Not the situation
100
- in really use case.
101
- email: hiroakisatou@example.com
96
+ description: Simple Input provides a fluent API for creating interactive command-line
97
+ prompts with validation and conversion support. Inspired by golang's huh! library's
98
+ input component.
99
+ email:
100
+ - ''
102
101
  executables: []
103
102
  extensions: []
104
103
  extra_rdoc_files: []
105
104
  files:
105
+ - LICENSE
106
+ - README.md
107
+ - examples/area_calculator.rb
108
+ - examples/area_calculator_spec.rb
109
+ - examples/basic.rb
110
+ - examples/convert.rb
111
+ - examples/custom_validators.rb
112
+ - examples/error_handling.rb
113
+ - examples/validation_patterns.rb
106
114
  - lib/simple_prompt.rb
107
115
  - lib/simple_prompt/default_validators.rb
108
116
  - lib/simple_prompt/input.rb
@@ -111,7 +119,9 @@ homepage: https://github.com/hiroakisatou/simple-prompt
111
119
  licenses:
112
120
  - MIT
113
121
  metadata:
122
+ homepage_uri: https://github.com/hiroakisatou/simple-prompt
114
123
  source_code_uri: https://github.com/hiroakisatou/simple-prompt
124
+ changelog_uri: https://github.com/hiroakisatou/simple-prompt/blob/main/CHANGELOG.md
115
125
  rubygems_mfa_required: 'true'
116
126
  rdoc_options: []
117
127
  require_paths:
@@ -120,7 +130,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
130
  requirements:
121
131
  - - ">="
122
132
  - !ruby/object:Gem::Version
123
- version: '3.4'
133
+ version: 3.4.0
124
134
  required_rubygems_version: !ruby/object:Gem::Requirement
125
135
  requirements:
126
136
  - - ">="
@@ -129,5 +139,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
139
  requirements: []
130
140
  rubygems_version: 4.0.6
131
141
  specification_version: 4
132
- summary: A simple input library for Ruby
142
+ summary: A simple prompt library for Ruby inspired by golang's huh! library
133
143
  test_files: []