clino 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d86d5698e312851641e338834676b2f93bcb7776af31f9422b0743853faeccf7
4
+ data.tar.gz: 338b0887ebde6716a29895038dc82c3782f5802b26ec5fac30ea646a7bafb62f
5
+ SHA512:
6
+ metadata.gz: 0bed4f9fcc8f06d4826825cff4c8e5f288268414faeca9696fe8afac0f8fc28acc77dfdb244247023dce2ea64289f9f07fa3aeb0f0d079b5eb30d1f3be316ca2
7
+ data.tar.gz: 46e68b260f20bc9fcd550c9be9306bcc9d72d7c32bf63cc6fea9ea8fd333e07459b8a3981f34c411b4c8b646e5c2f5751037d061d860533cab982d212d092de3
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,16 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
14
+
15
+ Style/Documentation:
16
+ Enabled: false
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at tikhon.zaikin@ematiq.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Tikhon Zaikin
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # Clino
2
+
3
+ Clino (originally Clinohumite) gem is a CLI builder library that provides a simple way to create a command line interface for your Ruby application.
4
+
5
+ It is inspired by the [Thor](https://github.com/rails/thor) and [Typer](https://github.com/tiangolo/typer) libraries, and aims to provide a simple and easy to use interface for building command line interfaces.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'clino'
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Interfaces
18
+ There are two interfaces available: `Min` and `Cli`.
19
+
20
+ #### Min
21
+ The `Min` interface is a minimalistic interface that provides a simple way to create a command line interface for your Ruby application.
22
+
23
+ It is inspired by the [Typer](https://github.com/tiangolo/typer) pythonic approach
24
+
25
+ It tries to unleash all the power of Ruby's metaprogramming capabilities to provide a simple and easy to use interface for building command line interfaces.
26
+
27
+ #### Cli
28
+ The `Cli` interface is a more advanced interface that provides a more complex way to create a command line interface for your Ruby application.
29
+
30
+ It is inspired by the [Thor](https://github.com/rails/thor) library and provides a more complex and feature-rich interface for building command line interfaces.
31
+
32
+ ### Examples
33
+
34
+ #### Hello World
35
+
36
+ Let's write a simple script that takes a name as an argument and prints a greeting message.
37
+
38
+ ```ruby
39
+ # hello.rb
40
+
41
+ require "clino/interfaces/min"
42
+
43
+ def hello(name)
44
+ "Hello, #{name}!"
45
+ end
46
+
47
+ Min.new(:hello).start
48
+ ```
49
+
50
+ Writing method containing only business logic is enough, as the input and output handling is done by the `Min` interface.
51
+
52
+ Run the script with the following command:
53
+
54
+ ```bash
55
+ ruby hello.rb World # => Hello, World!
56
+ ```
57
+
58
+ Get help with the following command:
59
+
60
+ ```bash
61
+ ruby hello.rb --help
62
+ ```
63
+
64
+ or
65
+
66
+ ```bash
67
+ ruby hello.rb --h
68
+ ```
69
+
70
+ It will print the following output:
71
+
72
+ ```bash
73
+ Script: hello.rb
74
+
75
+ Arguments:
76
+
77
+ <name> [string]
78
+
79
+ Usage: hello.rb [arguments] [options]
80
+ Use --h, --help to print this help message.
81
+ ```
82
+ #### Randomizer Minimalistic Example
83
+
84
+ Let's write a simple script that generates a random number within a given range with some conditions and transformations.
85
+
86
+ ```ruby
87
+ # min_randomizer.rb
88
+
89
+ require "clino/interfaces/min"
90
+
91
+ def generate_rnd_uni(from, to, incl)
92
+ range = incl ? from..to : from...to
93
+ rand(range)
94
+ end
95
+
96
+ def generate_rnd_exp(from, to, _incl)
97
+ mean = (from + to) / 2.0
98
+ -mean * Math.log(rand) if mean.positive?
99
+ end
100
+
101
+ def generate_rnd(from, to, mult = 1.0, alg:, incl: false)
102
+ from = load_input :integer, from
103
+ to = load_input :integer, to
104
+ mult = load_input :float, mult
105
+ incl = load_input :bool, incl
106
+
107
+ raise ArgumentError, "The lower bound (#{from}) must be less than the upper bound (#{to})" if from >= to
108
+ raise ArgumentError, "Algorithm must be one of: [uni, exp]" unless %w[uni exp].include?(alg)
109
+
110
+ send("generate_rnd_#{alg}", from, to, incl) * mult
111
+ end
112
+
113
+ Min.new(:generate_rnd).start
114
+ ```
115
+
116
+ As we can see, for more complex (or just non-string) input, we can use the `load_input` method to load the input and validate it.
117
+
118
+ Anyway, it requires us to write additional logic for validation and error handling.
119
+
120
+ Run the script with the following command:
121
+
122
+ ```bash
123
+ ruby min_randomizer.rb 1 10 --alg uni # => some number from [1.0, 9.0]
124
+ ```
125
+
126
+ Get help with the following command:
127
+
128
+ ```bash
129
+ ruby min_randomizer.rb --help
130
+ ```
131
+
132
+ or
133
+
134
+ ```bash
135
+ ruby min_randomizer.rb --h
136
+ ```
137
+
138
+ It will print the following output:
139
+
140
+ ```bash
141
+ Script: min_randomizer.rb
142
+
143
+ Arguments:
144
+
145
+ <from> [string]
146
+
147
+ <to> [string]
148
+
149
+ [<mult>] [string] [default: unknown]
150
+
151
+ Options:
152
+
153
+ --alg [string]
154
+
155
+ [--incl] [string] [default: unknown]
156
+
157
+ Usage: min_randomizer.rb [arguments] [options]
158
+ Use --h, --help to print this help message.
159
+ ```
160
+
161
+ As we can see, ruby's metaprogramming capabilities allow us to create a simple CLI, however it doesn't provide a way to handle complex input.
162
+
163
+ Unfortunately, ruby doesn't allow inspecting the method's signature default values, so we can handle input only as strings, and we can only show placeholders for default values this way.
164
+
165
+ #### Randomizer Advanced Example
166
+
167
+ Let's write an advanced version of the randomizer that has CLI signature.
168
+
169
+ ```ruby
170
+ # randomizer.rb
171
+
172
+ require "securerandom"
173
+ require "clino/interfaces/cli"
174
+
175
+ class RandomGeneratorCLI < Cli
176
+ # include Cli
177
+ desc <<-TEXT
178
+ Generate a random number between the given bounds
179
+ using the specified algorithm and multiply it by the given multiplier
180
+ TEXT
181
+ opt :alg, aliases: ["-a"], type: :string, desc: "Algorithm to use (uni or exp)"
182
+ opt :incl, aliases: ["-i"], type: :bool, default: false, desc: "Include upper bound"
183
+ arg :from, type: :integer, desc: "Lower bound"
184
+ arg :to, type: :integer, desc: "Upper bound"
185
+ arg :mult, type: :float, default: 1, desc: "Multiplier"
186
+
187
+ def run(from, to, mult, alg:, incl:)
188
+ raise ArgumentError, "The lower bound (#{from}) must be less than the upper bound (#{to})" if from >= to
189
+ raise ArgumentError, "Algorithm must be one of: [uni, exp]" unless %w[uni exp].include?(alg)
190
+
191
+ send("generate_rnd_#{alg}", from, to, incl) * mult
192
+ end
193
+
194
+ private
195
+
196
+ def generate_rnd_uni(from, to, incl)
197
+ range = incl ? from..to : from...to
198
+ rand(range)
199
+ end
200
+
201
+ def generate_rnd_exp(from, to, _incl)
202
+ mean = (from + to) / 2.0
203
+ -mean * Math.log(rand) if mean.positive?
204
+ end
205
+ end
206
+
207
+ RandomGeneratorCLI.new.start
208
+ ```
209
+
210
+ Run the script with the following command:
211
+
212
+ ```bash
213
+ ruby randomizer.rb 1 2 --alg uni --i y # => some number from [1.0, 2.0]
214
+ ```
215
+
216
+ Get help with the following command:
217
+
218
+ ```bash
219
+ ruby randomizer.rb --help
220
+ ```
221
+
222
+ or
223
+
224
+ ```bash
225
+ ruby randomizer.rb --h
226
+ ```
227
+
228
+ It will print the following output:
229
+
230
+ ```bash
231
+ Script: randomizer.rb
232
+
233
+ Description:
234
+ Generate a random number between the given bounds
235
+ using the specified algorithm and multiply it by the given multiplier
236
+
237
+
238
+ Arguments:
239
+ # Lower bound
240
+ <from> [integer]
241
+ # Upper bound
242
+ <to> [integer]
243
+ # Multiplier
244
+ [<mult>] [float] [default: 1.0]
245
+
246
+ Options:
247
+ # Algorithm to use (uni or exp)
248
+ --alg (-a) [string]
249
+ # Include upper bound
250
+ [--[no-]incl] (-[no-]i) [bool] [default: false]
251
+
252
+ Usage: randomizer.rb [arguments] [options]
253
+ Use --h, --help to print this help message.
254
+ ```
255
+
256
+ As we can see, all the default values, helping notes, and types are written out, and the input validation is handled automatically.
257
+
258
+ ## Plugins
259
+ ### Input Types
260
+
261
+ It is possible to create custom input types for the `Cli` interface.
262
+
263
+ ```ruby
264
+ # weight_calculator.rb
265
+
266
+ require "clino/interfaces/cli"
267
+ require "clino/plugins/input_types"
268
+
269
+ def convert_positive_integer(value)
270
+ raise ArgumentError, "Value must be a positive integer" unless value.to_i.positive?
271
+
272
+ value.to_i
273
+ end
274
+
275
+ INPUT_TYPES_PLUGIN.register(:positive_integer, method(:convert_positive_integer))
276
+
277
+ class IdealWeight < Cli
278
+ arg :height, type: :positive_integer
279
+
280
+ def run(height)
281
+ return height * 0.45 if height < 100
282
+
283
+ (height - 100) * 0.9
284
+ end
285
+ end
286
+
287
+ IdealWeight.new.start
288
+ ```
289
+
290
+ Run the script with the following command:
291
+
292
+ ```bash
293
+ ruby weight_calculator.rb 180 # => 72.0
294
+ ruby weight_calculator.rb -1 # => ArgumentError: Value must be a positive integer
295
+ ```
296
+
297
+ The type will be automatically registered and the help message will be printed as follows:
298
+
299
+ ```bash
300
+ Script: weight_calculator.rb
301
+
302
+ Arguments:
303
+
304
+ <height> [positive_integer]
305
+
306
+ Usage: weight_calculator.rb [arguments] [options]
307
+ Use --h, --help to print this help message.
308
+ ```
309
+
310
+ ## Improvements
311
+
312
+ - [ ] Add more tests
313
+ - [ ] Add better UI/UX in a sense of errors and help messages
314
+ - [ ] Fix problem with arguments beginning with "--"
315
+ - [ ] Add more input types (JSON?)
316
+ - [ ] Add CI/CD
317
+ - [ ] Add more examples
318
+ - [ ] Create a proper documentation
319
+ - [ ] Better boolean flag handling
320
+
321
+ ## Contributing
322
+
323
+ Bug reports and pull requests are welcome on GitHub at https://github.com/snusmumr1000/Clino. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/Clino/blob/main/CODE_OF_CONDUCT.md).
324
+
325
+ ## License
326
+
327
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
328
+
329
+ ## Code of Conduct
330
+
331
+ Everyone interacting in the Clino project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/snusmumr1000/Clino/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/clino.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/clino/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "clino"
7
+ spec.version = Clino::VERSION
8
+ spec.authors = ["Tikhon Zaikin"]
9
+ spec.email = ["snusmumrmail@gmail.com"]
10
+
11
+ spec.summary = "Min CLI generator"
12
+ spec.description = "clino is a minimalistic CLI generator
13
+ that allows you to create a CLI application with minimal effort."
14
+ spec.homepage = "https://github.com/snusmumr1000/Clino"
15
+ spec.license = "MIT"
16
+ spec.required_ruby_version = ">= 2.6.0"
17
+
18
+ # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0").reject do |f|
28
+ (File.expand_path(f) == __FILE__) ||
29
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
30
+ end
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ # Uncomment to register a new dependency of your gem
37
+ # spec.add_dependency "example-gem", "~> 1.0"
38
+
39
+ # For more information and examples about making a new gem, check out our
40
+ # guide at: https://bundler.io/guides/creating_gem.html
41
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ def general_strip(str, substring = " ")
4
+ substring_len = substring.length
5
+ str = str[substring_len..] while str.start_with?(substring)
6
+ str = str[0...-substring_len] while str.end_with?(substring)
7
+ str
8
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module InputParser
6
+ def parse_input(args_and_opts, signature)
7
+ input = {}
8
+ pos_args = []
9
+ args_and_opts.each do |arg|
10
+ break if arg.start_with?("--")
11
+
12
+ pos_args << arg
13
+ end
14
+
15
+ opts = args_and_opts[pos_args.length..]
16
+
17
+ pos_args.each_with_index do |val, idx|
18
+ arg = signature.args_arr[idx]
19
+ input[arg.name] = load_input arg.type, val if arg
20
+ end
21
+
22
+ OptionParser.new do |opt|
23
+ signature.opts.each_key do |name|
24
+ signature_opt = signature.opts[name]
25
+ type = signature_opt.type
26
+ aliases = signature_opt.aliases
27
+ if type == :bool
28
+ opt.on(*aliases, "--[no-]#{name}") do |v|
29
+ input[name] = v
30
+ end
31
+ next
32
+ end
33
+
34
+ if signature_opt.required?
35
+ opt.on(*aliases, "--#{name} #{name.upcase}") do |v|
36
+ input[name] = load_input signature.opts[name].type, v
37
+ end
38
+ else
39
+ opt.on(*aliases, "--#{name}") do |v|
40
+ input[name] = load_input signature.opts[name].type, v
41
+ end
42
+ end
43
+ end
44
+ opt.on("--h", "--help", "Prints this help") do
45
+ input[:help] = true
46
+ end
47
+ end.parse!(opts)
48
+ input
49
+ end
50
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ResultObtainer
4
+ def call_method_with_args(signature, method, input)
5
+ positional_values = []
6
+ keyword_values = {}
7
+
8
+ signature.args_arr.each do |arg|
9
+ if arg.required?
10
+ raise ArgumentError, "Missing required argument: #{arg.name}" unless input.key?(arg.name)
11
+
12
+ positional_values << input[arg.name]
13
+ else
14
+ positional_values << input[arg.name] unless input[arg.name].nil?
15
+ end
16
+ end
17
+
18
+ signature.opts.each do |opt_name, opt|
19
+ if opt.required?
20
+ keyword_values[opt_name] = input[opt_name]
21
+ else
22
+ keyword_values[opt_name] = input[opt_name] unless input[opt_name].nil?
23
+ end
24
+ end
25
+
26
+ default_opts = signature.default_opts
27
+ default_opts.each do |opt_name, opt|
28
+ keyword_values[opt_name] = opt unless keyword_values.key?(opt_name) || opt == :unknown
29
+ end
30
+
31
+ default_args = signature.default_args
32
+ last_default_args_needed = signature.args_arr.length - positional_values.length
33
+ positional_values += default_args[-last_default_args_needed..]
34
+ positional_values = positional_values.filter { |v| v != :unknown }
35
+
36
+ method.call(*positional_values, **keyword_values)
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Base
4
+ BaseSignatureStruct = Struct.new(:name, :type, :default, :desc)
5
+
6
+ module BaseSignatureValidator
7
+ def self.included(base)
8
+ base.class_eval do
9
+ # Override Struct's initialize method to include validation
10
+ def initialize(name:, type: :string, default: :none, desc: nil, **others)
11
+ super(name, type, default, desc, *others.values)
12
+ validate_name(name)
13
+ end
14
+ end
15
+ end
16
+
17
+ def validate_name(name)
18
+ raise ArgumentError, "Name must be a symbol" unless name.is_a?(Symbol)
19
+ end
20
+ end
21
+
22
+ module BaseSignature
23
+ include BaseSignatureValidator
24
+ def required?
25
+ default == :none
26
+ end
27
+ end
28
+
29
+ ArgSignature = Struct.new(*BaseSignatureStruct.members, :pos) do
30
+ include BaseSignature
31
+ def initialize(name:, type: :string, default: :none, desc: nil, pos: nil)
32
+ super
33
+ end
34
+ end
35
+
36
+ OptSignature = Struct.new(*BaseSignatureStruct.members, :aliases) do
37
+ include BaseSignature
38
+ def initialize(name:, type: :string, default: :none, desc: nil, aliases: [])
39
+ super
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../common/utils"
4
+ require_relative "../../plugins/input_types"
5
+
6
+ class CliSignature
7
+ attr_reader :args, :opts, :args_arr
8
+ attr_accessor :description
9
+
10
+ def initialize
11
+ @args = {}
12
+ @args_arr = []
13
+ @opts = {}
14
+ @description = nil
15
+ @help = nil
16
+ end
17
+
18
+ def add_arg(arg)
19
+ @args[arg.name] = arg
20
+ @args_arr[arg.pos] = arg
21
+ end
22
+
23
+ def add_opt(opt)
24
+ @opts[opt.name] = opt
25
+ end
26
+
27
+ def help
28
+ return @help unless @help.nil?
29
+
30
+ program_name = $PROGRAM_NAME
31
+ @help = "Script: #{program_name}\n"
32
+
33
+ unless @description.nil?
34
+ @help += "\nDescription:\n"
35
+ @help += " #{@description}\n"
36
+ end
37
+
38
+ unless @args_arr.empty?
39
+ @help += "\nArguments:\n"
40
+ @args_arr.each do |arg|
41
+ arg_part = " #{arg.required? ? "<#{arg.name}>" : "[<#{arg.name}>]"}"
42
+ arg_desc = arg.desc ? " # #{arg.desc}" : ""
43
+ arg_default = arg.default != :none ? " [default: #{load_input arg.type, arg.default}]" : ""
44
+ arg_type = " [#{arg.type}]"
45
+ @help += "#{arg_desc}\n#{arg_part}#{arg_type}#{arg_default}\n"
46
+ end
47
+ end
48
+
49
+ unless @opts.empty?
50
+ @help += "\nOptions:\n"
51
+ @opts.each do |name, option|
52
+ name = "[no-]#{name}" if option.type == :bool
53
+ opt_part = " #{option.required? ? "--#{name}" : "[--#{name}]"}"
54
+ if option.aliases && !option.aliases.empty?
55
+ aliases = option.aliases.join(", ")
56
+ opt_part += " (#{aliases})"
57
+ end
58
+ opt_desc = option.desc ? " # #{option.desc}" : ""
59
+ opt_default = option.default != :none ? " [default: #{option.default}]" : ""
60
+ opt_type = " [#{option.type}]"
61
+ @help += "#{opt_desc}\n#{opt_part}#{opt_type} #{opt_default}\n"
62
+ end
63
+ end
64
+
65
+ @help += "\nUsage: #{program_name} [arguments] [options]"
66
+
67
+ @help += "\nUse --h, --help to print this help message.\n"
68
+
69
+ @help
70
+ end
71
+
72
+ def self.from_func(func)
73
+ arg_types = %i[req opt]
74
+ opt_types = %i[key keyreq]
75
+ signature = CliSignature.new
76
+ func.parameters.each_with_index do |param, idx|
77
+ param_type, param_name = param
78
+ if arg_types.include?(param_type)
79
+ arg = Base::ArgSignature.new(
80
+ name: param_name,
81
+ pos: idx
82
+ )
83
+ arg.default = :unknown if param_type == :opt
84
+ signature.add_arg arg
85
+ elsif opt_types.include?(param_type)
86
+ opt = Base::OptSignature.new(
87
+ name: param_name
88
+ )
89
+ opt.default = :unknown if param_type == :key
90
+ signature.add_opt opt
91
+ end
92
+ end
93
+ signature
94
+ end
95
+
96
+ def default_opts
97
+ opts = {}
98
+ @opts.each do |name, opt|
99
+ opts[name] = opt.default unless opt.required?
100
+ end
101
+ opts
102
+ end
103
+
104
+ def default_args
105
+ args = []
106
+ @args_arr.each do |arg|
107
+ args << arg.default unless arg.required?
108
+ end
109
+ args
110
+ end
111
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BaseCli
4
+ include InputParser
5
+ include ResultObtainer
6
+
7
+ def start(args = ARGV)
8
+ input = parse_input args, @signature
9
+
10
+ if input[:help]
11
+ print_help
12
+ return
13
+ end
14
+
15
+ print_result call_method_with_args @signature, @method, input
16
+ end
17
+
18
+ def print_help
19
+ puts @signature.help
20
+ end
21
+
22
+ def print_result(result)
23
+ puts result
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/signature/cli_signature"
4
+ require_relative "../domain/signature/base"
5
+ require_relative "../domain/input_parser"
6
+ require_relative "../domain/result_obtainer"
7
+ require_relative "base/base_cli"
8
+
9
+ class Cli
10
+ class << self
11
+ attr_reader :opt_buffer, :arg_buffer, :description
12
+
13
+ def desc(description)
14
+ @description = description
15
+ end
16
+
17
+ def opt(name, type: :string, desc: nil, aliases: [], default: :none)
18
+ @opt_buffer ||= {}
19
+ bool_prefix = type == :bool ? "[no-]" : ""
20
+ aliases = aliases.map { |a| "-#{bool_prefix}#{general_strip a, "-"}" }
21
+ default = load_input type, default unless default == :none
22
+ @opt_buffer[name] = Base::OptSignature.new(name: name, type: type, default: default, desc: desc,
23
+ aliases: aliases)
24
+ end
25
+
26
+ def arg(name, type: :string, desc: nil, default: :none, pos: nil)
27
+ @arg_buffer ||= {}
28
+ default = load_input type, default unless default == :none
29
+ @arg_buffer[name] = Base::ArgSignature.new(name: name, type: type, default: default, desc: desc, pos: pos)
30
+ end
31
+ end
32
+
33
+ def initialize
34
+ @method = method(:run)
35
+ @signature ||= CliSignature.from_func @method
36
+ @signature.args_arr.each do |arg|
37
+ self.class.arg_buffer[arg.name].pos = arg.pos
38
+ @signature.add_arg(self.class.arg_buffer[arg.name])
39
+ end
40
+
41
+ @signature.opts.each_key do |name|
42
+ @signature.add_opt(self.class.opt_buffer[name])
43
+ end
44
+
45
+ @signature.description = self.class.description
46
+ end
47
+
48
+ include BaseCli
49
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/signature/base"
4
+ require_relative "../domain/input_parser"
5
+ require_relative "../domain/signature/cli_signature"
6
+ require_relative "../domain/result_obtainer"
7
+ require_relative "../plugins/input_types"
8
+ require_relative "base/base_cli"
9
+ require "optparse"
10
+
11
+ class Min
12
+ include BaseCli
13
+
14
+ def initialize(command)
15
+ @method = method command if command.is_a? Symbol
16
+ @method = command if command.is_a? Method
17
+
18
+ @signature = CliSignature.from_func @method
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InputTypes
4
+ def initialize
5
+ @converters = {}
6
+ register(:float, method(:convert_float))
7
+ register(:integer, method(:convert_integer))
8
+ register(:bool, method(:convert_bool))
9
+ register(:string, method(:convert_string))
10
+ end
11
+
12
+ def register(type, converter)
13
+ raise ArgumentError, "Invalid converter" unless converter.respond_to?(:call)
14
+ raise ArgumentError, "Invalid type" unless type.is_a?(Symbol)
15
+
16
+ @converters[type] = converter
17
+ end
18
+
19
+ def convert(type, value)
20
+ raise ArgumentError, "Invalid type: #{type}, available types: #{@converters.keys}" unless @converters.key?(type)
21
+
22
+ @converters[type].call(value)
23
+ end
24
+
25
+ def convert_float(value = nil)
26
+ return nil if value.nil?
27
+
28
+ value.to_f
29
+ end
30
+
31
+ def convert_integer(value = nil)
32
+ return nil if value.nil?
33
+
34
+ value.to_i
35
+ end
36
+
37
+ def convert_bool(value = nil)
38
+ return nil if value.nil?
39
+
40
+ if [true, false].include?(value)
41
+ value
42
+ else
43
+ true_values = %w[true yes 1 t y]
44
+ falsy_values = %w[false no 0 f n]
45
+ if true_values.include?(value.downcase)
46
+ true
47
+ elsif falsy_values.include?(value.downcase)
48
+ false
49
+ else
50
+ raise ArgumentError, "Invalid boolean value: #{default}"
51
+ end
52
+ end
53
+ end
54
+
55
+ def convert_string(value = nil)
56
+ return nil if value.nil?
57
+
58
+ value.to_s
59
+ end
60
+ end
61
+
62
+ INPUT_TYPES_PLUGIN = InputTypes.new
63
+
64
+ def load_input(type, value)
65
+ INPUT_TYPES_PLUGIN.convert(type, value)
66
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clino
4
+ VERSION = "0.1.0"
5
+ end
data/lib/clino.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "clino/version"
4
+
5
+ # clino
6
+ module Clino
7
+ end
data/sig/clino.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Clino
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clino
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tikhon Zaikin
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-02-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |-
14
+ clino is a minimalistic CLI generator
15
+ that allows you to create a CLI application with minimal effort.
16
+ email:
17
+ - snusmumrmail@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".rspec"
23
+ - ".rubocop.yml"
24
+ - CODE_OF_CONDUCT.md
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - clino.gemspec
29
+ - lib/clino.rb
30
+ - lib/clino/common/utils.rb
31
+ - lib/clino/domain/input_parser.rb
32
+ - lib/clino/domain/result_obtainer.rb
33
+ - lib/clino/domain/signature/base.rb
34
+ - lib/clino/domain/signature/cli_signature.rb
35
+ - lib/clino/interfaces/base/base_cli.rb
36
+ - lib/clino/interfaces/cli.rb
37
+ - lib/clino/interfaces/min.rb
38
+ - lib/clino/plugins/input_types.rb
39
+ - lib/clino/version.rb
40
+ - sig/clino.rbs
41
+ homepage: https://github.com/snusmumr1000/Clino
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ homepage_uri: https://github.com/snusmumr1000/Clino
46
+ source_code_uri: https://github.com/snusmumr1000/Clino
47
+ changelog_uri: https://github.com/snusmumr1000/Clino/blob/main/CHANGELOG.md
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.6.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.4.10
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Min CLI generator
67
+ test_files: []