toys-core 0.14.7 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toys
4
+ module Utils
5
+ ##
6
+ # An object that implements standard UI elements, such as error reports and
7
+ # logging, as provided by the `toys` command line. Specifically, it
8
+ # implements pretty formatting of log entries and stack traces, and renders
9
+ # using ANSI coloring where available via {Toys::Utils::Terminal}.
10
+ #
11
+ # This object can be used to implement `toys`-style behavior when creating
12
+ # a CLI object. For example:
13
+ #
14
+ # require "toys/utils/standard_ui"
15
+ # ui = Toys::Utils::StandardUI.new
16
+ # cli = Toys::CLI.new(**ui.cli_args)
17
+ #
18
+ class StandardUI
19
+ ##
20
+ # Create a Standard UI.
21
+ #
22
+ # By default, all output is written to `$stderr`, and will share a single
23
+ # {Toys::Utils::Terminal} object, allowing multiple tools and/or threads
24
+ # to interleave messages without interrupting one another.
25
+ #
26
+ # @param output [IO,Toys::Utils::Terminal] Where to write output. You can
27
+ # pass a terminal object, or an IO stream that will be wrapped in a
28
+ # terminal output. Default is `$stderr`.
29
+ #
30
+ def initialize(output: nil)
31
+ @terminal = output || $stderr
32
+ require "toys/utils/terminal"
33
+ @terminal = Terminal.new(output: @terminal) unless @terminal.is_a?(Terminal)
34
+ @log_header_severity_styles = {
35
+ "FATAL" => [:bright_magenta, :bold, :underline],
36
+ "ERROR" => [:bright_red, :bold],
37
+ "WARN" => [:bright_yellow],
38
+ "INFO" => [:bright_cyan],
39
+ "DEBUG" => [:white],
40
+ }
41
+ end
42
+
43
+ ##
44
+ # The terminal underlying this UI
45
+ #
46
+ # @return [Toys::Utils::Terminal]
47
+ #
48
+ attr_reader :terminal
49
+
50
+ ##
51
+ # A hash that maps severities to styles recognized by
52
+ # {Toys::Utils::Terminal}. Used to style the header for each log entry.
53
+ # This hash can be modified in place to adjust the behavior of loggers
54
+ # created by this UI.
55
+ #
56
+ # @return [Hash{String => Array<Symbol>}]
57
+ #
58
+ attr_reader :log_header_severity_styles
59
+
60
+ ##
61
+ # Convenience method that returns a hash of arguments that can be passed
62
+ # to the {Toys::CLI} constructor. Includes the `:error_handler` and
63
+ # `:logger_factory` arguments.
64
+ #
65
+ # @return [Hash]
66
+ #
67
+ def cli_args
68
+ {
69
+ error_handler: error_handler,
70
+ logger_factory: logger_factory,
71
+ }
72
+ end
73
+
74
+ ##
75
+ # Returns an error handler conforming to the `:error_handler` argument to
76
+ # the {Toys::CLI} constructor. Specifically, it returns the
77
+ # {#error_handler_impl} method as a proc.
78
+ #
79
+ # @return [Proc]
80
+ #
81
+ def error_handler
82
+ @error_handler ||= method(:error_handler_impl).to_proc
83
+ end
84
+
85
+ ##
86
+ # Returns a logger factory conforming to the `:logger_factory` argument
87
+ # to the {Toys::CLI} constructor. Specifically, it returns the
88
+ # {#logger_factory_impl} method as a proc.
89
+ #
90
+ # @return [Proc]
91
+ #
92
+ def logger_factory
93
+ @logger_factory ||= method(:logger_factory_impl).to_proc
94
+ end
95
+
96
+ ##
97
+ # Implementation of the error handler. As dictated by the error handler
98
+ # specification in {Toys::CLI}, this must take a {Toys::ContextualError}
99
+ # as an argument, and return an exit code.
100
+ #
101
+ # The base implementation uses {#display_error_notice} and
102
+ # {#display_signal_notice} to print an appropriate message to the UI's
103
+ # terminal, and uses {#exit_code_for} to determine the correct exit code.
104
+ # Any of those methods can be overridden by a subclass to alter their
105
+ # behavior, or this main implementation method can be overridden to
106
+ # change the overall behavior.
107
+ #
108
+ # @param error [Toys::ContextualError] The error received
109
+ # @return [Integer] The exit code
110
+ #
111
+ def error_handler_impl(error)
112
+ cause = error.cause
113
+ if cause.is_a?(::SignalException)
114
+ display_signal_notice(cause)
115
+ else
116
+ display_error_notice(error)
117
+ end
118
+ exit_code_for(cause)
119
+ end
120
+
121
+ ##
122
+ # Implementation of the logger factory. As dictated by the logger factory
123
+ # specification in {Toys::CLI}, this must take a {Toys::ToolDefinition}
124
+ # as an argument, and return a `Logger`.
125
+ #
126
+ # The base implementation returns a logger that writes to the UI's
127
+ # terminal, using {#logger_formatter_impl} as the formatter. It sets the
128
+ # level to `Logger::WARN` by default. Either this method or the helper
129
+ # methods can be overridden to change this behavior.
130
+ #
131
+ # @param _tool {Toys::ToolDefinition} The tool definition of the tool to
132
+ # be executed
133
+ # @return [Logger]
134
+ #
135
+ def logger_factory_impl(_tool)
136
+ logger = ::Logger.new(@terminal)
137
+ logger.formatter = method(:logger_formatter_impl).to_proc
138
+ logger.level = ::Logger::WARN
139
+ logger
140
+ end
141
+
142
+ ##
143
+ # Returns an exit code appropriate for the given exception. Currently,
144
+ # the logic interprets signals (returning the convention of 128 + signo),
145
+ # usage errors (returning the conventional value of 2), and tool not
146
+ # runnable errors (returning the conventional value of 126), and defaults
147
+ # to 1 for all other error types.
148
+ #
149
+ # This method is used by {#error_handler_impl} and can be overridden to
150
+ # change its behavior.
151
+ #
152
+ # @param error [Exception] The exception raised. This method expects the
153
+ # original exception, rather than a ContextualError.
154
+ # @return [Integer] The appropriate exit code
155
+ #
156
+ def exit_code_for(error)
157
+ case error
158
+ when ArgParsingError
159
+ 2
160
+ when NotRunnableError
161
+ 126
162
+ when ::SignalException
163
+ error.signo + 128
164
+ else
165
+ 1
166
+ end
167
+ end
168
+
169
+ ##
170
+ # Displays a default output for a signal received.
171
+ #
172
+ # This method is used by {#error_handler_impl} and can be overridden to
173
+ # change its behavior.
174
+ #
175
+ # @param error [SignalException]
176
+ #
177
+ def display_signal_notice(error)
178
+ @terminal.puts
179
+ if error.is_a?(::Interrupt)
180
+ @terminal.puts("INTERRUPTED", :bold)
181
+ else
182
+ @terminal.puts("SIGNAL RECEIVED: #{error.signm || error.signo}", :bold)
183
+ end
184
+ end
185
+
186
+ ##
187
+ # Displays a default output for an error. Displays the error, the
188
+ # backtrace, and contextual information regarding what tool was run and
189
+ # where in its code the error occurred.
190
+ #
191
+ # This method is used by {#error_handler_impl} and can be overridden to
192
+ # change its behavior.
193
+ #
194
+ # @param error [Toys::ContextualError]
195
+ #
196
+ def display_error_notice(error)
197
+ @terminal.puts
198
+ @terminal.puts(cause_string(error.cause))
199
+ @terminal.puts(context_string(error), :bold)
200
+ end
201
+
202
+ ##
203
+ # Implementation of the formatter used by loggers created by this UI's
204
+ # logger factory. This interface is defined by the standard `Logger`
205
+ # class.
206
+ #
207
+ # This method can be overridden to change the behavior of loggers created
208
+ # by this UI.
209
+ #
210
+ # @param severity [String]
211
+ # @param time [Time]
212
+ # @param _progname [String]
213
+ # @param msg [Object]
214
+ # @return [String]
215
+ #
216
+ def logger_formatter_impl(severity, time, _progname, msg)
217
+ msg_str =
218
+ case msg
219
+ when ::String
220
+ msg
221
+ when ::Exception
222
+ "#{msg.message} (#{msg.class})\n" << (msg.backtrace || []).join("\n")
223
+ else
224
+ msg.inspect
225
+ end
226
+ timestr = time.strftime("%Y-%m-%d %H:%M:%S")
227
+ header = format("[%<time>s %<sev>5s]", time: timestr, sev: severity)
228
+ styles = log_header_severity_styles[severity]
229
+ header = @terminal.apply_styles(header, *styles) if styles
230
+ "#{header} #{msg_str}\n"
231
+ end
232
+
233
+ private
234
+
235
+ def cause_string(cause)
236
+ lines = ["#{cause.class}: #{cause.message}"]
237
+ cause.backtrace.each_with_index.reverse_each do |bt, i|
238
+ lines << " #{(i + 1).to_s.rjust(3)}: #{bt}"
239
+ end
240
+ lines.join("\n")
241
+ end
242
+
243
+ def context_string(error)
244
+ lines = [
245
+ error.banner || "Unexpected error!",
246
+ " #{error.cause.class}: #{error.cause.message}",
247
+ ]
248
+ if error.config_path
249
+ lines << " in config file: #{error.config_path}:#{error.config_line}"
250
+ end
251
+ if error.tool_name
252
+ lines << " while executing tool: #{error.tool_name.join(' ').inspect}"
253
+ if error.tool_args
254
+ lines << " with arguments: #{error.tool_args.inspect}"
255
+ end
256
+ end
257
+ lines.join("\n")
258
+ end
259
+ end
260
+ end
261
+ end
data/lib/toys-core.rb CHANGED
@@ -12,10 +12,51 @@
12
12
  # This module contains the command line framework underlying Toys. It can be
13
13
  # used to create command line executables using the Toys DSL and classes.
14
14
  #
15
+ # ## Common starting points
16
+ #
17
+ # Some of the most commonly needed class documentation is listed below:
18
+ #
19
+ # * For information on the DSL used to write tools, start with
20
+ # {Toys::DSL::Tool}.
21
+ # * The base class for tool runtime (i.e. that defines the basic methods
22
+ # available to a tool's implementation) is {Toys::Context}.
23
+ # * For information on writing mixins, see {Toys::Mixin}.
24
+ # * For information on writing templates, see {Toys::Template}.
25
+ # * For information on writing acceptors, see {Toys::Acceptor}.
26
+ # * For information on writing custom shell completions, see {Toys::Completion}.
27
+ # * Standard mixins are defined under the {Toys::StandardMixins} module.
28
+ # * Various utilities are defined under {Toys::Utils}. Some of these serve as
29
+ # the implementations of corresponding mixins.
30
+ # * The main entrypoint for the command line framework is {Toys::CLI}.
31
+ #
32
+ # Other important internal classes are listed below.
33
+ #
34
+ # * The definition of a tool is represented by {Toys::ToolDefinition} along
35
+ # the helpers {Toys::Flag}, {Toys::PositionalArg}, and {Toys::FlagGroup}.
36
+ # * Argument parsing is implemented by {Toys::ArgParser}.
37
+ # * The process of finding and loading a tool definition given a tool name, is
38
+ # implemented by {Toys::Loader}.
39
+ # * Text wrapping is handled by {Toys::WrappableString}.
40
+ # * The settings system is implemented by {Toys::Settings}.
41
+ #
15
42
  module Toys
16
43
  ##
17
44
  # Namespace for DSL classes. These classes provide the directives that can be
18
- # used in configuration files. Most are defined in {Toys::DSL::Tool}.
45
+ # used in configuration files.
46
+ #
47
+ # DSL directives that can appear at the top level of Toys files and tool
48
+ # blocks are defined by the {Toys::DSL::Tool} module.
49
+ #
50
+ # Directives that can appear within a block passed to {Toys::DSL::Tool#flag}
51
+ # are defined by the {Toys::DSL::Flag} class.
52
+ #
53
+ # Directives that can appear within a {Toys::DSL::Tool#flag_group} block or
54
+ # any of its related directives, are defined by the {Toys::DSL::FlagGroup}
55
+ # class.
56
+ #
57
+ # Directives that can appear within a {Toys::DSL::Tool#required_arg},
58
+ # {Toys::DSL::Tool#optional_arg}, or {Toys::DSL::Tool#remaining_args} block,
59
+ # are defined by the {Toys::DSL::PositionalArg} class.
19
60
  #
20
61
  module DSL
21
62
  end
@@ -23,6 +64,9 @@ module Toys
23
64
  ##
24
65
  # Namespace for standard middleware classes.
25
66
  #
67
+ # These middleware are provided by Toys-Core and can be referenced by name
68
+ # when creating a {Toys::CLI}.
69
+ #
26
70
  module StandardMiddleware
27
71
  ##
28
72
  # @private
@@ -42,6 +86,9 @@ module Toys
42
86
  ##
43
87
  # Namespace for standard mixin classes.
44
88
  #
89
+ # These mixins are provided by Toys-Core and can be included by name by
90
+ # passing a symbol to {Toys::DSL::Tool#include}.
91
+ #
45
92
  module StandardMixins
46
93
  end
47
94
 
@@ -49,8 +96,9 @@ module Toys
49
96
  # Namespace for common utility classes.
50
97
  #
51
98
  # These classes are not loaded by default, and must be required explicitly.
52
- # For example, before using {Toys::Utils::Exec}, you must
53
- # `require "toys/utils/exec"`.
99
+ # For example, before using {Toys::Utils::Exec}, you must:
100
+ #
101
+ # require "toys/utils/exec"
54
102
  #
55
103
  module Utils
56
104
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toys-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.7
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-19 00:00:00.000000000 Z
11
+ date: 2023-10-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Toys-Core is the command line tool framework underlying Toys. It can
14
14
  be used to create command line executables using the Toys DSL and classes.
@@ -71,6 +71,7 @@ files:
71
71
  - lib/toys/utils/git_cache.rb
72
72
  - lib/toys/utils/help_text.rb
73
73
  - lib/toys/utils/pager.rb
74
+ - lib/toys/utils/standard_ui.rb
74
75
  - lib/toys/utils/terminal.rb
75
76
  - lib/toys/utils/xdg.rb
76
77
  - lib/toys/wrappable_string.rb
@@ -78,10 +79,10 @@ homepage: https://github.com/dazuma/toys
78
79
  licenses:
79
80
  - MIT
80
81
  metadata:
81
- changelog_uri: https://dazuma.github.io/toys/gems/toys-core/v0.14.7/file.CHANGELOG.html
82
+ changelog_uri: https://dazuma.github.io/toys/gems/toys-core/v0.15.0/file.CHANGELOG.html
82
83
  source_code_uri: https://github.com/dazuma/toys/tree/main/toys-core
83
84
  bug_tracker_uri: https://github.com/dazuma/toys/issues
84
- documentation_uri: https://dazuma.github.io/toys/gems/toys-core/v0.14.7
85
+ documentation_uri: https://dazuma.github.io/toys/gems/toys-core/v0.15.0
85
86
  post_install_message:
86
87
  rdoc_options: []
87
88
  require_paths:
@@ -97,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
98
  - !ruby/object:Gem::Version
98
99
  version: '0'
99
100
  requirements: []
100
- rubygems_version: 3.1.6
101
+ rubygems_version: 3.4.10
101
102
  signing_key:
102
103
  specification_version: 4
103
104
  summary: Framework for creating command line executables