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 +4 -4
- data/LICENSE +21 -0
- data/README.md +320 -0
- data/examples/area_calculator.rb +65 -0
- data/examples/area_calculator_spec.rb +54 -0
- data/examples/basic.rb +31 -0
- data/examples/convert.rb +58 -0
- data/examples/custom_validators.rb +35 -0
- data/examples/error_handling.rb +38 -0
- data/examples/validation_patterns.rb +50 -0
- data/lib/simple_prompt/input.rb +20 -6
- data/lib/simple_prompt/version.rb +1 -1
- metadata +20 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 067ce86ac266a279491b935536c473fe2b257e9a3cd3556877c7affe2be3311c
|
|
4
|
+
data.tar.gz: e0f078fa1ee462081ed0d2da0d76f44461b6e90d1d595cf1beea1186574937e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)"
|
data/examples/convert.rb
ADDED
|
@@ -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})"
|
data/lib/simple_prompt/input.rb
CHANGED
|
@@ -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.
|
|
22
|
-
@writer = T.let($stdout, T.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
82
|
-
def with_context(
|
|
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
|
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.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hiroaki Satou
|
|
8
|
-
bindir:
|
|
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:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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:
|
|
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
|
|
142
|
+
summary: A simple prompt library for Ruby inspired by golang's huh! library
|
|
133
143
|
test_files: []
|