hanami-cli 0.0.0 → 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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