toys-core 0.14.7 → 0.15.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.
@@ -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