hanami-cli 0.0.0 → 0.1.0.beta1

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.
@@ -0,0 +1,184 @@
1
+ require "concurrent/hash"
2
+
3
+ module Hanami
4
+ class CLI
5
+ # Command registry
6
+ #
7
+ # @since 0.1.0
8
+ # @api private
9
+ class CommandRegistry
10
+ # @since 0.1.0
11
+ # @api private
12
+ def initialize
13
+ @root = Node.new
14
+ end
15
+
16
+ # @since 0.1.0
17
+ # @api private
18
+ def set(name, command, aliases, **options)
19
+ node = @root
20
+ command = command_for(name, command, **options)
21
+ name.split(/[[:space:]]/).each do |token|
22
+ node = node.put(node, token)
23
+ end
24
+
25
+ node.aliases!(aliases)
26
+ node.leaf!(command) unless command.nil?
27
+
28
+ nil
29
+ end
30
+
31
+ # @since 0.1.0
32
+ # @api private
33
+ def get(arguments)
34
+ node = @root
35
+ args = []
36
+ names = []
37
+ result = LookupResult.new(node, args, names, node.leaf?)
38
+
39
+ arguments.each_with_index do |token, i|
40
+ tmp = node.lookup(token)
41
+
42
+ if tmp.nil?
43
+ result = LookupResult.new(node, args, names, false)
44
+ break
45
+ elsif tmp.leaf?
46
+ args = arguments[i + 1..-1]
47
+ names = arguments[0..i]
48
+ node = tmp
49
+ result = LookupResult.new(node, args, names, true)
50
+ break
51
+ else
52
+ names = arguments[0..i]
53
+ node = tmp
54
+ result = LookupResult.new(node, args, names, node.leaf?)
55
+ end
56
+ end
57
+
58
+ result
59
+ end
60
+
61
+ private
62
+
63
+ # @since 0.1.0
64
+ # @api private
65
+ def command_for(name, command, **options)
66
+ if command.nil?
67
+ command
68
+ else
69
+ command.new(command_name: name, **options)
70
+ end
71
+ end
72
+
73
+ # Node of the registry
74
+ #
75
+ # @since 0.1.0
76
+ # @api private
77
+ class Node
78
+ # @since 0.1.0
79
+ # @api private
80
+ attr_reader :parent
81
+
82
+ # @since 0.1.0
83
+ # @api private
84
+ attr_reader :children
85
+
86
+ # @since 0.1.0
87
+ # @api private
88
+ attr_reader :aliases
89
+
90
+ # @since 0.1.0
91
+ # @api private
92
+ attr_reader :command
93
+
94
+ # @since 0.1.0
95
+ # @api private
96
+ def initialize(parent = nil)
97
+ @parent = parent
98
+ @children = Concurrent::Hash.new
99
+ @aliases = Concurrent::Hash.new
100
+ @command = nil
101
+ end
102
+
103
+ # @since 0.1.0
104
+ # @api private
105
+ def put(parent, key)
106
+ children[key] ||= self.class.new(parent)
107
+ end
108
+
109
+ # @since 0.1.0
110
+ # @api private
111
+ def lookup(token)
112
+ children[token] || aliases[token]
113
+ end
114
+
115
+ # @since 0.1.0
116
+ # @api private
117
+ def leaf!(command)
118
+ @command = command
119
+ end
120
+
121
+ # @since 0.1.0
122
+ # @api private
123
+ def alias!(key, child)
124
+ @aliases[key] = child
125
+ end
126
+
127
+ # @since 0.1.0
128
+ # @api private
129
+ def aliases!(aliases)
130
+ aliases.each do |a|
131
+ parent.alias!(a, self)
132
+ end
133
+ end
134
+
135
+ # @since 0.1.0
136
+ # @api private
137
+ def leaf?
138
+ !command.nil?
139
+ end
140
+ end
141
+
142
+ # Result of a registry lookup
143
+ #
144
+ # @since 0.1.0
145
+ # @api private
146
+ class LookupResult
147
+ # @since 0.1.0
148
+ # @api private
149
+ attr_reader :names
150
+
151
+ # @since 0.1.0
152
+ # @api private
153
+ attr_reader :arguments
154
+
155
+ # @since 0.1.0
156
+ # @api private
157
+ def initialize(node, arguments, names, found)
158
+ @node = node
159
+ @arguments = arguments
160
+ @names = names
161
+ @found = found
162
+ end
163
+
164
+ # @since 0.1.0
165
+ # @api private
166
+ def found?
167
+ @found
168
+ end
169
+
170
+ # @since 0.1.0
171
+ # @api private
172
+ def children
173
+ @node.children
174
+ end
175
+
176
+ # @since 0.1.0
177
+ # @api private
178
+ def command
179
+ @node.command
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,125 @@
1
+ require "hanami/utils/string"
2
+
3
+ module Hanami
4
+ class CLI
5
+ # Command line option
6
+ #
7
+ # @since 0.1.0
8
+ # @api private
9
+ class Option
10
+ # @since 0.1.0
11
+ # @api private
12
+ attr_reader :name
13
+
14
+ # @since 0.1.0
15
+ # @api private
16
+ attr_reader :options
17
+
18
+ # @since 0.1.0
19
+ # @api private
20
+ def initialize(name, options = {})
21
+ @name = name
22
+ @options = options
23
+ end
24
+
25
+ # @since 0.1.0
26
+ # @api private
27
+ def aliases
28
+ options[:aliases] || []
29
+ end
30
+
31
+ # @since 0.1.0
32
+ # @api private
33
+ def desc
34
+ desc = options[:desc]
35
+ values ? "#{desc}: (#{values.join('/')})" : desc
36
+ end
37
+
38
+ # @since 0.1.0
39
+ # @api private
40
+ def required?
41
+ options[:required]
42
+ end
43
+
44
+ # @since 0.1.0
45
+ # @api private
46
+ def type
47
+ options[:type]
48
+ end
49
+
50
+ # @since 0.1.0
51
+ # @api private
52
+ def values
53
+ options[:values]
54
+ end
55
+
56
+ # @since 0.1.0
57
+ # @api private
58
+ def boolean?
59
+ type == :boolean
60
+ end
61
+
62
+ # @since 0.1.0
63
+ # @api private
64
+ def default
65
+ options[:default]
66
+ end
67
+
68
+ # @since 0.1.0
69
+ # @api private
70
+ def description_name
71
+ options[:label] || name.upcase
72
+ end
73
+
74
+ # @since 0.1.0
75
+ # @api private
76
+ def argument?
77
+ false
78
+ end
79
+
80
+ # @since 0.1.0
81
+ # @api private
82
+ #
83
+ # rubocop:disable Metrics/AbcSize
84
+ # rubocop:disable Metrics/MethodLength
85
+ def parser_options
86
+ dasherized_name = Hanami::Utils::String.dasherize(name)
87
+ parser_options = []
88
+
89
+ if type == :boolean
90
+ parser_options << "--[no-]#{dasherized_name}"
91
+ else
92
+ parser_options << "--#{dasherized_name}=#{name}"
93
+ parser_options << "--#{dasherized_name} #{name}"
94
+ end
95
+
96
+ parser_options << values if values
97
+ parser_options.unshift(alias_name) unless alias_name.nil?
98
+ parser_options << desc if desc
99
+ parser_options
100
+ end
101
+ # rubocop:enable Metrics/MethodLength
102
+ # rubocop:enable Metrics/AbcSize
103
+
104
+ private
105
+
106
+ # @since 0.1.0
107
+ # @api private
108
+ def alias_name
109
+ aliases.join(" ") if aliases.any?
110
+ end
111
+ end
112
+
113
+ # Command line argument
114
+ #
115
+ # @since 0.1.0
116
+ # @api private
117
+ class Argument < Option
118
+ # @since 0.1.0
119
+ # @api private
120
+ def argument?
121
+ true
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,123 @@
1
+ require "optparse"
2
+ require "hanami/cli/program_name"
3
+
4
+ module Hanami
5
+ class CLI
6
+ # Parse command line arguments and options
7
+ #
8
+ # @since 0.1.0
9
+ # @api private
10
+ module Parser
11
+ # @since 0.1.0
12
+ # @api private
13
+ #
14
+ # rubocop:disable Metrics/AbcSize
15
+ # rubocop:disable Metrics/MethodLength
16
+ def self.call(command, arguments, names)
17
+ parsed_options = {}
18
+
19
+ OptionParser.new do |opts|
20
+ command.options.each do |option|
21
+ opts.on(*option.parser_options) do |value|
22
+ parsed_options[option.name.to_sym] = value
23
+ end
24
+ end
25
+
26
+ opts.on_tail("-h", "--help") do
27
+ return Result.help
28
+ end
29
+ end.parse!(arguments.dup)
30
+
31
+ parsed_options = command.default_params.merge(parsed_options)
32
+ parse_required_params(command, arguments, names, parsed_options)
33
+ rescue ::OptionParser::ParseError
34
+ return Result.failure
35
+ end
36
+ # rubocop:enable Metrics/MethodLength
37
+ # rubocop:enable Metrics/AbcSize
38
+
39
+ # @since 0.1.0
40
+ # @api private
41
+ def self.full_command_name(names)
42
+ ProgramName.call(names)
43
+ end
44
+
45
+ # @since 0.1.0
46
+ # @api private
47
+ #
48
+ # rubocop:disable Metrics/AbcSize
49
+ # rubocop:disable Metrics/MethodLength
50
+ def self.parse_required_params(command, arguments, names, parsed_options)
51
+ parse_params = Hash[command.arguments.map(&:name).zip(arguments)]
52
+ parse_required_params = Hash[command.required_arguments.map(&:name).zip(arguments)]
53
+ all_required_params_satisfied = command.required_arguments.all? { |param| !parse_required_params[param.name].nil? }
54
+
55
+ unless all_required_params_satisfied
56
+ parse_required_params_values = parse_required_params.values.compact
57
+
58
+ usage = "\nUsage: \"#{full_command_name(names)} #{command.required_arguments.map(&:description_name).join(' ')}\""
59
+
60
+ if parse_required_params_values.empty? # rubocop:disable Style/GuardClause
61
+ return Result.failure("ERROR: \"#{full_command_name(names)}\" was called with no arguments#{usage}")
62
+ else
63
+ return Result.failure("ERROR: \"#{full_command_name(names)}\" was called with arguments #{parse_required_params_values}#{usage}")
64
+ end
65
+ end
66
+
67
+ Result.success(parse_params.merge(parsed_options))
68
+ end
69
+ # rubocop:enable Metrics/MethodLength
70
+ # rubocop:enable Metrics/AbcSize
71
+
72
+ # @since 0.1.0
73
+ # @api private
74
+ class Result
75
+ # @since 0.1.0
76
+ # @api private
77
+ def self.help
78
+ new(help: true)
79
+ end
80
+
81
+ # @since 0.1.0
82
+ # @api private
83
+ def self.success(arguments = {})
84
+ new(arguments: arguments)
85
+ end
86
+
87
+ # @since 0.1.0
88
+ # @api private
89
+ def self.failure(error = "Error: Invalid param provided")
90
+ new(error: error)
91
+ end
92
+
93
+ # @since 0.1.0
94
+ # @api private
95
+ attr_reader :arguments
96
+
97
+ # @since 0.1.0
98
+ # @api private
99
+ attr_reader :error
100
+
101
+ # @since 0.1.0
102
+ # @api private
103
+ def initialize(arguments: {}, error: nil, help: false)
104
+ @arguments = arguments
105
+ @error = error
106
+ @help = help
107
+ end
108
+
109
+ # @since 0.1.0
110
+ # @api private
111
+ def error?
112
+ !error.nil?
113
+ end
114
+
115
+ # @since 0.1.0
116
+ # @api private
117
+ def help?
118
+ @help
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,19 @@
1
+ module Hanami
2
+ class CLI
3
+ # Program name
4
+ #
5
+ # @since 0.1.0
6
+ # @api private
7
+ module ProgramName
8
+ # @since 0.1.0
9
+ # @api private
10
+ SEPARATOR = " ".freeze
11
+
12
+ # @since 0.1.0
13
+ # @api private
14
+ def self.call(names = [], program_name: $PROGRAM_NAME)
15
+ [File.basename(program_name), names].flatten.join(SEPARATOR)
16
+ end
17
+ end
18
+ end
19
+ end