simple_input 0.1.0 → 0.2.1

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: e065b4936a53b4c1e0b9f2332af8209ee1bd533843df9586bf79c20fb7c57674
4
- data.tar.gz: c66dc78ffed023c2f649c3e90ac8ebec510535241b2dab29b2ddaa7d9d41c843
3
+ metadata.gz: 7bc4897e13a98263144b74d7f41c52a5551a5fd82137700fc81f7fef5ca27271
4
+ data.tar.gz: 111cec79eef33fe0c657f79d42a29020b8764880513df2839d676fc926bf96bf
5
5
  SHA512:
6
- metadata.gz: 4d0732167d96ad22769951c189cc3885b3d5e5eecb3fdea3f0b882f538818fe7f1ba31c7b94dce35ad27988d361c73dc02929895b115bf4a1ff323d57ecc9b6d
7
- data.tar.gz: 1d9b4eb5f0fc7f9d6ea0f185a9d5de5c19a827d91e8ae3ac6cfa98ddff3bb7f0c3254c26ed4d0ad80dec7b837cd51405bc03b90e691aff521f001ef55547a81a
6
+ metadata.gz: f2a86db2a363fdef0d0b75c71b9c9b962af329a171f10ae97879a52950e6a75374ed569645217ec32356ff637f1b34ba085edf242040681be60eeed56635e335
7
+ data.tar.gz: 6a6b0061f158c692db24a9ab149ad79d71ea0d49adb49f43bef20973299cd40a7b7443962e45bf4ed467418ff1dd1537426362f2ad1031d68124b32f4ef6b803
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,231 @@
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
+ ## Running Examples
171
+
172
+ ```bash
173
+ # Basic usage and validation
174
+ ruby examples/basic.rb
175
+
176
+ # Custom validator providers
177
+ ruby examples/custom_validators.rb
178
+
179
+ # Value conversion (string to int, array, etc.)
180
+ ruby examples/convert.rb
181
+
182
+ # Common validation patterns (email, password, range, choice)
183
+ ruby examples/validation_patterns.rb
184
+
185
+ # Error handling and conversion safety
186
+ ruby examples/error_handling.rb
187
+ ```
188
+
189
+ ## Testing
190
+
191
+ ```bash
192
+ bundle exec rspec
193
+ ```
194
+
195
+ ### Testing with `with_context` (Private Method)
196
+
197
+ 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`:
198
+
199
+ ```ruby
200
+ def build_input(input_text)
201
+ reader = StringIO.new(input_text)
202
+ writer = StringIO.new
203
+ input = Input.new_input
204
+ input.send(:with_context, reader, writer)
205
+ [input, writer]
206
+ end
207
+
208
+ # Usage in tests
209
+ it 'returns the user input' do
210
+ input, = build_input("hello\n")
211
+ result = input.run
212
+ expect(result).to eq('hello')
213
+ end
214
+
215
+ it 'shows validation errors' do
216
+ input, writer = build_input("\nhello\n")
217
+ result = input.validate(:not_empty).run
218
+ expect(result).to eq('hello')
219
+ expect(writer.string).to include('入力してください')
220
+ end
221
+ ```
222
+
223
+ This pattern lets you:
224
+ - Simulate user input via `StringIO` as the reader
225
+ - Capture all terminal output via `StringIO` as the writer
226
+ - Test the full input loop including validation re-prompting without interactive terminal input
227
+
228
+
229
+ ## License
230
+
231
+ MIT
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})"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimplePrompt
4
- VERSION = "0.1.0"
4
+ VERSION = '0.2.1'
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.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
- - tsu-na-gu
8
- bindir: bin
7
+ - Hiroaki Satou
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -93,20 +93,34 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: 0.5.100
96
- description: A simple input library for Ruby, inspired by golang's huh! library's
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
97
98
  input component.
99
+ email:
100
+ - ''
98
101
  executables: []
99
102
  extensions: []
100
103
  extra_rdoc_files: []
101
104
  files:
105
+ - LICENSE
106
+ - README.md
107
+ - examples/basic.rb
108
+ - examples/convert.rb
109
+ - examples/custom_validators.rb
110
+ - examples/error_handling.rb
111
+ - examples/validation_patterns.rb
102
112
  - lib/simple_prompt.rb
103
113
  - lib/simple_prompt/default_validators.rb
104
114
  - lib/simple_prompt/input.rb
105
115
  - lib/simple_prompt/version.rb
106
- homepage: https://github.com/tsu-na-gu/simple-input
116
+ homepage: https://github.com/hiroakisatou/simple-prompt
107
117
  licenses:
108
118
  - MIT
109
- metadata: {}
119
+ metadata:
120
+ homepage_uri: https://github.com/hiroakisatou/simple-prompt
121
+ source_code_uri: https://github.com/hiroakisatou/simple-prompt
122
+ changelog_uri: https://github.com/hiroakisatou/simple-prompt/blob/main/CHANGELOG.md
123
+ rubygems_mfa_required: 'true'
110
124
  rdoc_options: []
111
125
  require_paths:
112
126
  - lib
@@ -114,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
128
  requirements:
115
129
  - - ">="
116
130
  - !ruby/object:Gem::Version
117
- version: '3.4'
131
+ version: 3.4.0
118
132
  required_rubygems_version: !ruby/object:Gem::Requirement
119
133
  requirements:
120
134
  - - ">="
@@ -123,5 +137,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
137
  requirements: []
124
138
  rubygems_version: 4.0.6
125
139
  specification_version: 4
126
- summary: A simple input library for Ruby
140
+ summary: A simple prompt library for Ruby inspired by golang's huh! library
127
141
  test_files: []