pakyow-core 0.11.3 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE +4 -0
  4. data/{pakyow-core/README.md → README.md} +3 -4
  5. data/commands/pakyow +13 -0
  6. data/lib/pakyow/app/connection/behavior/session.rb +33 -0
  7. data/lib/pakyow/app/connection/behavior/values.rb +42 -0
  8. data/lib/pakyow/app/connection/behavior/verifier.rb +34 -0
  9. data/lib/pakyow/app/connection/session/abstract.rb +24 -0
  10. data/lib/pakyow/app/connection/session/cookie.rb +56 -0
  11. data/lib/pakyow/app/connection.rb +57 -0
  12. data/lib/pakyow/app.rb +219 -0
  13. data/lib/pakyow/behavior/aspects.rb +45 -0
  14. data/lib/pakyow/behavior/config.rb +72 -0
  15. data/lib/pakyow/behavior/endpoints.rb +41 -0
  16. data/lib/pakyow/behavior/frameworks.rb +45 -0
  17. data/lib/pakyow/behavior/helpers.rb +107 -0
  18. data/lib/pakyow/behavior/initializers.rb +19 -0
  19. data/lib/pakyow/behavior/isolating.rb +69 -0
  20. data/lib/pakyow/behavior/operations.rb +46 -0
  21. data/lib/pakyow/behavior/pipeline.rb +58 -0
  22. data/lib/pakyow/behavior/plugins.rb +148 -0
  23. data/lib/pakyow/behavior/rescuing.rb +51 -0
  24. data/lib/pakyow/behavior/restarting.rb +68 -0
  25. data/lib/pakyow/behavior/sessions.rb +34 -0
  26. data/lib/pakyow/behavior/verification.rb +47 -0
  27. data/lib/pakyow/cli.rb +199 -0
  28. data/lib/pakyow/connection/multipart_input.rb +24 -0
  29. data/lib/pakyow/connection/multipart_parser.rb +112 -0
  30. data/lib/pakyow/connection/params.rb +29 -0
  31. data/lib/pakyow/connection/query_parser.rb +174 -0
  32. data/lib/pakyow/connection/statuses.rb +119 -0
  33. data/lib/pakyow/connection.rb +568 -0
  34. data/lib/pakyow/endpoints.rb +94 -0
  35. data/lib/pakyow/environment/actions/dispatch.rb +37 -0
  36. data/lib/pakyow/environment/actions/input_parser.rb +21 -0
  37. data/lib/pakyow/environment/actions/logger.rb +33 -0
  38. data/lib/pakyow/environment/actions/normalizer.rb +73 -0
  39. data/lib/pakyow/environment/behavior/config.rb +166 -0
  40. data/lib/pakyow/environment/behavior/initializers.rb +21 -0
  41. data/lib/pakyow/environment/behavior/input_parsing.rb +54 -0
  42. data/lib/pakyow/environment/behavior/plugins.rb +23 -0
  43. data/lib/pakyow/environment/behavior/restarting.rb +54 -0
  44. data/lib/pakyow/environment/behavior/running.rb +129 -0
  45. data/lib/pakyow/environment/behavior/silencing.rb +23 -0
  46. data/lib/pakyow/environment/behavior/timezone.rb +21 -0
  47. data/lib/pakyow/environment/behavior/watching.rb +75 -0
  48. data/lib/pakyow/environment.rb +381 -0
  49. data/lib/pakyow/error.rb +273 -0
  50. data/lib/pakyow/errors.rb +97 -0
  51. data/lib/pakyow/framework.rb +39 -0
  52. data/lib/pakyow/generator.rb +138 -0
  53. data/lib/pakyow/generators/project/default/%dot%env.erb +1 -0
  54. data/lib/pakyow/generators/project/default/%dot%gitignore +11 -0
  55. data/lib/pakyow/generators/project/default/%dot%ruby-version.erb +1 -0
  56. data/lib/pakyow/generators/project/default/Gemfile.erb +28 -0
  57. data/lib/pakyow/generators/project/default/README.md +14 -0
  58. data/lib/pakyow/generators/project/default/config/application.rb.erb +17 -0
  59. data/lib/pakyow/generators/project/default/config/environment.rb +11 -0
  60. data/lib/pakyow/generators/project/default/config/initializers/application/keep +0 -0
  61. data/lib/pakyow/generators/project/default/config/initializers/environment/keep +0 -0
  62. data/lib/pakyow/generators/project/default/database/keep +0 -0
  63. data/lib/pakyow/generators/project/default/frontend/assets/images/keep +0 -0
  64. data/lib/pakyow/generators/project/default/frontend/assets/packs/keep +0 -0
  65. data/lib/pakyow/generators/project/default/frontend/assets/styles/keep +0 -0
  66. data/lib/pakyow/generators/project/default/frontend/includes/keep +0 -0
  67. data/lib/pakyow/generators/project/default/frontend/layouts/default.html.erb +19 -0
  68. data/lib/pakyow/generators/project/default/frontend/pages/keep +0 -0
  69. data/lib/pakyow/generators/project/default/public/favicon.ico +0 -0
  70. data/lib/pakyow/generators/project/default/public/robots.txt +1 -0
  71. data/lib/pakyow/generators/project.rb +29 -0
  72. data/lib/pakyow/helpers/app.rb +28 -0
  73. data/lib/pakyow/helpers/connection.rb +45 -0
  74. data/lib/pakyow/info.rb +26 -0
  75. data/lib/pakyow/integrations/bootsnap.rb +18 -0
  76. data/lib/pakyow/integrations/bundler/require.rb +7 -0
  77. data/lib/pakyow/integrations/bundler/setup.rb +7 -0
  78. data/lib/pakyow/integrations/dotenv.rb +9 -0
  79. data/lib/pakyow/loader.rb +31 -0
  80. data/lib/pakyow/logger/colorizer.rb +38 -0
  81. data/lib/pakyow/logger/destination.rb +17 -0
  82. data/lib/pakyow/logger/formatter.rb +22 -0
  83. data/lib/pakyow/logger/formatters/human.rb +101 -0
  84. data/lib/pakyow/logger/formatters/json.rb +99 -0
  85. data/lib/pakyow/logger/formatters/logfmt.rb +50 -0
  86. data/lib/pakyow/logger/multiplexed.rb +19 -0
  87. data/lib/pakyow/logger/thread_local.rb +29 -0
  88. data/lib/pakyow/logger/timekeeper.rb +64 -0
  89. data/lib/pakyow/logger.rb +125 -0
  90. data/lib/pakyow/operation.rb +71 -0
  91. data/lib/pakyow/plugin/helper_caller.rb +20 -0
  92. data/lib/pakyow/plugin/lookup.rb +33 -0
  93. data/lib/pakyow/plugin/state.rb +68 -0
  94. data/lib/pakyow/plugin.rb +367 -0
  95. data/lib/pakyow/process_manager.rb +53 -0
  96. data/lib/pakyow/processes/proxy.rb +67 -0
  97. data/lib/pakyow/processes/server.rb +32 -0
  98. data/lib/pakyow/rack/compatibility.rb +68 -0
  99. data/lib/pakyow/task.rb +315 -0
  100. data/lib/pakyow/tasks/boot.rake +21 -0
  101. data/lib/pakyow/tasks/create.rake +32 -0
  102. data/lib/pakyow/tasks/help.rake +17 -0
  103. data/lib/pakyow/tasks/info/endpoints.rake +72 -0
  104. data/lib/pakyow/tasks/info.rake +31 -0
  105. data/lib/pakyow/tasks/irb.rake +11 -0
  106. data/lib/pakyow/tasks/prelaunch.rake +37 -0
  107. data/lib/pakyow/types.rb +32 -0
  108. data/lib/pakyow/validations/acceptance.rb +37 -0
  109. data/lib/pakyow/validations/email.rb +28 -0
  110. data/lib/pakyow/validations/inline.rb +32 -0
  111. data/lib/pakyow/validations/length.rb +44 -0
  112. data/lib/pakyow/validations/presence.rb +41 -0
  113. data/lib/pakyow/validations.rb +8 -0
  114. data/lib/pakyow/validator.rb +81 -0
  115. data/lib/pakyow/verifier.rb +177 -0
  116. data/lib/pakyow.rb +5 -0
  117. metadata +267 -65
  118. data/pakyow-core/CHANGELOG.md +0 -128
  119. data/pakyow-core/LICENSE +0 -20
  120. data/pakyow-core/lib/pakyow/core/app.rb +0 -82
  121. data/pakyow-core/lib/pakyow/core/app_context.rb +0 -10
  122. data/pakyow-core/lib/pakyow/core/base.rb +0 -61
  123. data/pakyow-core/lib/pakyow/core/call_context.rb +0 -171
  124. data/pakyow-core/lib/pakyow/core/config/app.rb +0 -49
  125. data/pakyow-core/lib/pakyow/core/config/cookies.rb +0 -4
  126. data/pakyow-core/lib/pakyow/core/config/logger.rb +0 -34
  127. data/pakyow-core/lib/pakyow/core/config/reloader.rb +0 -10
  128. data/pakyow-core/lib/pakyow/core/config/server.rb +0 -10
  129. data/pakyow-core/lib/pakyow/core/config/session.rb +0 -41
  130. data/pakyow-core/lib/pakyow/core/config.rb +0 -95
  131. data/pakyow-core/lib/pakyow/core/errors.rb +0 -16
  132. data/pakyow-core/lib/pakyow/core/helpers/configuring.rb +0 -142
  133. data/pakyow-core/lib/pakyow/core/helpers/hooks.rb +0 -106
  134. data/pakyow-core/lib/pakyow/core/helpers/running.rb +0 -124
  135. data/pakyow-core/lib/pakyow/core/helpers.rb +0 -61
  136. data/pakyow-core/lib/pakyow/core/loader.rb +0 -32
  137. data/pakyow-core/lib/pakyow/core/middleware/logger.rb +0 -146
  138. data/pakyow-core/lib/pakyow/core/middleware/override.rb +0 -3
  139. data/pakyow-core/lib/pakyow/core/middleware/reloader.rb +0 -23
  140. data/pakyow-core/lib/pakyow/core/middleware/req_path_normalizer.rb +0 -49
  141. data/pakyow-core/lib/pakyow/core/middleware/session.rb +0 -5
  142. data/pakyow-core/lib/pakyow/core/middleware/static.rb +0 -76
  143. data/pakyow-core/lib/pakyow/core/multilog.rb +0 -19
  144. data/pakyow-core/lib/pakyow/core/request.rb +0 -119
  145. data/pakyow-core/lib/pakyow/core/response.rb +0 -135
  146. data/pakyow-core/lib/pakyow/core/route_eval.rb +0 -254
  147. data/pakyow-core/lib/pakyow/core/route_expansion_eval.rb +0 -115
  148. data/pakyow-core/lib/pakyow/core/route_lookup.rb +0 -41
  149. data/pakyow-core/lib/pakyow/core/route_merger.rb +0 -76
  150. data/pakyow-core/lib/pakyow/core/route_module.rb +0 -21
  151. data/pakyow-core/lib/pakyow/core/route_set.rb +0 -74
  152. data/pakyow-core/lib/pakyow/core/route_template_defaults.rb +0 -43
  153. data/pakyow-core/lib/pakyow/core/route_template_eval.rb +0 -72
  154. data/pakyow-core/lib/pakyow/core/router.rb +0 -181
  155. data/pakyow-core/lib/pakyow/core.rb +0 -8
  156. data/pakyow-core/lib/pakyow-core.rb +0 -1
@@ -0,0 +1,381 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+
5
+ require "console"
6
+ require "console/split"
7
+
8
+ require "pakyow/support/core_refinements/array/ensurable"
9
+
10
+ require "pakyow/support/hookable"
11
+ require "pakyow/support/configurable"
12
+ require "pakyow/support/class_state"
13
+ require "pakyow/support/deep_dup"
14
+ require "pakyow/support/deep_freeze"
15
+ require "pakyow/support/logging"
16
+ require "pakyow/support/pipeline"
17
+ require "pakyow/support/inflector"
18
+
19
+ require "pakyow/environment/behavior/config"
20
+ require "pakyow/environment/behavior/initializers"
21
+ require "pakyow/environment/behavior/input_parsing"
22
+ require "pakyow/environment/behavior/plugins"
23
+ require "pakyow/environment/behavior/silencing"
24
+ require "pakyow/environment/behavior/timezone"
25
+ require "pakyow/environment/behavior/running"
26
+ require "pakyow/environment/behavior/watching"
27
+ require "pakyow/environment/behavior/restarting"
28
+
29
+ require "pakyow/environment/actions/dispatch"
30
+ require "pakyow/environment/actions/input_parser"
31
+ require "pakyow/environment/actions/logger"
32
+ require "pakyow/environment/actions/normalizer"
33
+
34
+ require "pakyow/app"
35
+
36
+ require "pakyow/logger/destination"
37
+ require "pakyow/logger/multiplexed"
38
+ require "pakyow/logger/thread_local"
39
+
40
+ # Pakyow environment for running one or more rack apps. Multiple apps can be
41
+ # mounted in the environment, each one handling requests at some path.
42
+ #
43
+ # Pakyow.configure do
44
+ # mount Pakyow::App, at: "/"
45
+ # end
46
+ #
47
+ # = Configuration
48
+ #
49
+ # The environment can be configured
50
+ #
51
+ # Pakyow.configure do
52
+ # config.server.port = 2001
53
+ # end
54
+ #
55
+ # It's possible to configure environments differently.
56
+ #
57
+ # Pakyow.configure :development do
58
+ # config.server.host = "pakyow.dev"
59
+ # end
60
+ #
61
+ # @see Support::Configurable
62
+ #
63
+ # = Hooks
64
+ #
65
+ # Hooks can be defined for the following events:
66
+ #
67
+ # - load
68
+ # - configure
69
+ # - setup
70
+ # - boot
71
+ # - shutdown
72
+ # - run
73
+ #
74
+ # Here's how to log a message after boot:
75
+ #
76
+ # Pakyow.after "boot" do
77
+ # logger.info "booted"
78
+ # end
79
+ #
80
+ # @see Support::Hookable
81
+ #
82
+ # = Logging
83
+ #
84
+ # The environment contains a global general-purpose logger. It also provides
85
+ # a {RequestLogger} instance to each app for logging during a request.
86
+ #
87
+ # = Setup & Running
88
+ #
89
+ # The environment can be setup and then run.
90
+ #
91
+ # Pakyow.setup(env: :development).run
92
+ #
93
+ module Pakyow
94
+ using Support::DeepDup
95
+ using Support::DeepFreeze
96
+ using Support::Refinements::Array::Ensurable
97
+
98
+ extend Support::DeepFreeze
99
+ unfreezable :global_logger, :app
100
+
101
+ include Support::Hookable
102
+ events :load, :configure, :setup, :boot, :shutdown, :run
103
+
104
+ include Support::Configurable
105
+
106
+ include Environment::Behavior::Config
107
+ include Environment::Behavior::Initializers
108
+ include Environment::Behavior::InputParsing
109
+ include Environment::Behavior::Plugins
110
+ include Environment::Behavior::Silencing
111
+ include Environment::Behavior::Timezone
112
+ include Environment::Behavior::Running
113
+ include Environment::Behavior::Watching
114
+ include Environment::Behavior::Restarting
115
+
116
+ include Support::Pipeline
117
+ action Actions::Logger
118
+ action Actions::Normalizer
119
+ action Actions::InputParser
120
+ action Actions::Dispatch
121
+
122
+ extend Support::ClassState
123
+ class_state :apps, default: []
124
+ class_state :tasks, default: []
125
+ class_state :mounts, default: []
126
+ class_state :frameworks, default: {}
127
+ class_state :booted, default: false, getter: false
128
+ class_state :server, default: nil, getter: false
129
+ class_state :env, default: nil, getter: false
130
+ class_state :setup_error, default: nil
131
+
132
+ class << self
133
+ # Name of the environment
134
+ #
135
+ attr_reader :env
136
+
137
+ # Logger instance for the environment
138
+ #
139
+ attr_reader :logger
140
+
141
+ # Global logger instance
142
+ #
143
+ attr_reader :global_logger
144
+
145
+ # Any error encountered during the boot process
146
+ #
147
+ attr_reader :error
148
+
149
+ # Mounts an app at a path.
150
+ #
151
+ # The app can be any rack endpoint, but must implement an
152
+ # initializer like {App#initialize}.
153
+ #
154
+ # @param app the rack endpoint to mount
155
+ # @param at [String] where the endpoint should be mounted
156
+ #
157
+ def mount(app, at:, &block)
158
+ mounts << { app: app, block: block, path: at }
159
+ end
160
+
161
+ # Loads the Pakyow environment for the current project.
162
+ #
163
+ def load
164
+ performing :load do
165
+ if File.exist?(config.loader_path + ".rb")
166
+ require config.loader_path
167
+ else
168
+ require "pakyow/integrations/bundler/setup"
169
+ require "pakyow/integrations/bootsnap"
170
+
171
+ require "pakyow/integrations/bundler/require"
172
+ require "pakyow/integrations/dotenv"
173
+
174
+ require config.environment_path
175
+
176
+ load_apps
177
+ end
178
+ end
179
+ end
180
+
181
+ # Loads apps located in the current project.
182
+ #
183
+ def load_apps
184
+ require "./config/application"
185
+ end
186
+
187
+ # Prepares the environment for booting.
188
+ #
189
+ # @param env [Symbol] the environment to prepare for
190
+ #
191
+ def setup(env: nil)
192
+ @env = (env ||= config.default_env).to_sym
193
+
194
+ load
195
+
196
+ performing :configure do
197
+ configure!(env)
198
+ $LOAD_PATH.unshift(config.lib)
199
+ end
200
+
201
+ performing :setup do
202
+ init_global_logger
203
+ end
204
+
205
+ self
206
+ rescue => error
207
+ begin
208
+ # Try again to initialize the logger, since we may have failed before that point.
209
+ #
210
+ unless Pakyow.logger
211
+ init_global_logger
212
+ end
213
+ rescue
214
+ end
215
+
216
+ @setup_error = error; self
217
+ end
218
+
219
+ # Returns true if the environment has booted.
220
+ #
221
+ def booted?
222
+ @booted == true
223
+ end
224
+
225
+ # Boots the environment without running it.
226
+ #
227
+ def boot(unsafe: false)
228
+ ensure_setup_succeeded
229
+
230
+ performing :boot do
231
+ # Tasks should only be available before boot.
232
+ #
233
+ @tasks = [] unless unsafe
234
+
235
+ # Mount each app.
236
+ #
237
+ @apps = mounts.map { |mount|
238
+ initialize_app_for_mount(mount)
239
+ }
240
+
241
+ # Create the callable pipeline.
242
+ #
243
+ @pipeline = Pakyow.__pipeline.callable(self)
244
+
245
+ # Set the environment as booted ahead of telling each app that it is booted. This allows an
246
+ # app's after boot hook to access the booted app through `Pakyow.app`.
247
+ #
248
+ @booted = true
249
+
250
+ # Now tell each app that it has been booted.
251
+ #
252
+ @apps.select { |app| app.respond_to?(:booted) }.each(&:booted)
253
+ end
254
+
255
+ if config.freeze_on_boot
256
+ deep_freeze unless unsafe
257
+ end
258
+
259
+ self
260
+ rescue StandardError => error
261
+ handle_boot_failure(error)
262
+ end
263
+
264
+ def register_framework(framework_name, framework_module)
265
+ @frameworks[framework_name] = framework_module
266
+ end
267
+
268
+ def app(app_name, path: "/", without: [], only: nil, mount: true, &block)
269
+ app_name = app_name.to_sym
270
+
271
+ if booted?
272
+ @apps.find { |app|
273
+ app.config.name == app_name
274
+ }
275
+ else
276
+ local_frameworks = (only || frameworks.keys) - Array.ensure(without)
277
+
278
+ Pakyow::App.make(Support::ObjectName.namespace(app_name, "app")) {
279
+ config.name = app_name
280
+ include_frameworks(*local_frameworks)
281
+ }.tap do |app|
282
+ app.define(&block) if block_given?
283
+ mount(app, at: path) if mount
284
+ end
285
+ end
286
+ end
287
+
288
+ def env?(name)
289
+ env == name.to_sym
290
+ end
291
+
292
+ def call(input)
293
+ config.connection_class.new(input).yield_self { |connection|
294
+ Async(logger: connection.logger) {
295
+ # Set the request logger as a thread-local variable for when there's no other way to access
296
+ # it. This originated when looking for a way to log queries with the request logger. By
297
+ # setting the request logger for the current connection as thread-local we can create a
298
+ # connection pointing to `Pakyow.logger`, an instance of `Pakyow::Logger::ThreadLocal`. The
299
+ # thread local logger decides at the time of logging which logger to use based on an
300
+ # available context, falling back to `Pakyow.global_logger`. This gets us around needing to
301
+ # configure a connection per request, altering Sequel's internals, and other oddities.
302
+ #
303
+ # Pakyow is designed so that the connection object and its logger should always be available
304
+ # anywhere you need it. If it isn't, reconsider the design before using the thread local.
305
+ #
306
+ Thread.current[:pakyow_logger] = connection.logger
307
+
308
+ catch :halt do
309
+ @pipeline.call(connection)
310
+ end
311
+ }.wait
312
+ }.finalize
313
+ rescue StandardError => error
314
+ Pakyow.logger.houston(error)
315
+
316
+ Async::HTTP::Protocol::Response.new(
317
+ nil, 500, {}, Async::HTTP::Body::Buffered.wrap(
318
+ StringIO.new("500 Low-Level Server Error")
319
+ )
320
+ )
321
+ end
322
+
323
+ # @api private
324
+ def load_tasks
325
+ require "rake"
326
+ require "pakyow/task"
327
+
328
+ @tasks = config.tasks.paths.uniq.each_with_object([]) do |tasks_path, tasks|
329
+ Dir.glob(File.join(File.expand_path(tasks_path), "**/*.rake")).each do |task_path|
330
+ tasks.concat(Pakyow::Task::Loader.new(task_path).__tasks)
331
+ end
332
+ end
333
+ end
334
+
335
+ # @api private
336
+ def initialize_app_for_mount(mount)
337
+ if mount[:app].ancestors.include?(Pakyow::App)
338
+ mount[:app].new(env, mount_path: mount[:path], &mount[:block])
339
+ else
340
+ mount[:app].new
341
+ end
342
+ end
343
+
344
+ private
345
+
346
+ def init_global_logger
347
+ destinations = Logger::Multiplexed.new(
348
+ *config.logger.destinations.map { |destination, io|
349
+ io.sync = config.logger.sync
350
+ Logger::Destination.new(destination, io)
351
+ }
352
+ )
353
+
354
+ @global_logger = config.logger.formatter.new(destinations)
355
+
356
+ @logger = Logger::ThreadLocal.new(
357
+ Logger.new("pkyw", output: @global_logger, level: config.logger.level)
358
+ )
359
+
360
+ Console.logger = Logger.new("asnc", output: @global_logger, level: :warn)
361
+ end
362
+
363
+ def ensure_setup_succeeded
364
+ if @setup_error
365
+ handle_boot_failure(@setup_error)
366
+ end
367
+ end
368
+
369
+ def handle_boot_failure(error)
370
+ @error = error
371
+
372
+ Support::Logging.safe do |logger|
373
+ logger.houston(error)
374
+ end
375
+
376
+ if config.exit_on_boot_failure
377
+ exit(false)
378
+ end
379
+ end
380
+ end
381
+ end
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "method_source"
5
+
6
+ require "pakyow/support/class_state"
7
+ require "pakyow/support/cli/style"
8
+ require "pakyow/support/dependencies"
9
+ require "pakyow/support/inflector"
10
+ require "pakyow/support/string_builder"
11
+
12
+ module Pakyow
13
+ # Base error object.
14
+ #
15
+ class Error < StandardError
16
+ class << self
17
+ # Wraps an error in a pakyow error instance, with additional context.
18
+ #
19
+ def build(original_error, message_type = :default, context: nil, **message_values)
20
+ if original_error.is_a?(self)
21
+ original_error
22
+ else
23
+ message = message(message_type, **message_values)
24
+ message = original_error.message if message.empty?
25
+ new(message).tap do |error|
26
+ error.wrapped_exception = original_error
27
+ error.set_backtrace(original_error.backtrace)
28
+ error.context = context
29
+ end
30
+ end
31
+ end
32
+
33
+ # Initialize an error with a particular message.
34
+ #
35
+ def new_with_message(type = :default, **values)
36
+ new(message(type, **values))
37
+ end
38
+
39
+ private
40
+
41
+ def message(type = :default, **values)
42
+ if @messages.include?(type)
43
+ Support::StringBuilder.new(
44
+ @messages[type]
45
+ ).build(**values)
46
+ else
47
+ ""
48
+ end
49
+ end
50
+ end
51
+
52
+ extend Support::ClassState
53
+ class_state :messages, default: {}, inheritable: true
54
+
55
+ attr_accessor :wrapped_exception, :context
56
+
57
+ def initialize(*)
58
+ @context = nil
59
+
60
+ super
61
+ end
62
+
63
+ def cause
64
+ wrapped_exception || super
65
+ end
66
+
67
+ def name
68
+ Support.inflector.humanize(
69
+ Support.inflector.underscore(
70
+ Support.inflector.demodulize(self.class.name)
71
+ )
72
+ )
73
+ end
74
+
75
+ def details
76
+ if project? && location = project_backtrace_locations[0]
77
+ message = "`#{(cause || self).class}' occurred on line `#{location.lineno}' of `#{path}':"
78
+
79
+ begin
80
+ <<~MESSAGE
81
+ #{message}
82
+
83
+ #{indent_as_source(MethodSource.source_helper([path, location.lineno], location.label), location.lineno)}
84
+ MESSAGE
85
+ rescue StandardError
86
+ <<~MESSAGE
87
+ #{message}
88
+
89
+ Error parsing source.
90
+ MESSAGE
91
+ end
92
+ elsif location = (cause || self).backtrace_locations.to_a[0]
93
+ library_name = Support::Dependencies.library_name(location.absolute_path)
94
+ library_type = Support::Dependencies.library_type(location.absolute_path)
95
+
96
+ occurred_in = if library_type == :pakyow || library_name.start_with?("pakyow-")
97
+ "within the `#{library_name.split("-", 2)[1]}' framework"
98
+ elsif library_type == :gem || library_type == :bundler
99
+ "within the `#{library_name}' gem"
100
+ else
101
+ "somewhere within ruby itself"
102
+ end
103
+
104
+ <<~MESSAGE
105
+ `#{(cause || self).class}' occurred outside of your project, #{occurred_in}.
106
+ MESSAGE
107
+ else
108
+ <<~MESSAGE
109
+ `#{(cause || self).class}' occurred at an unknown location.
110
+ MESSAGE
111
+ end
112
+ end
113
+
114
+ # If the error occurred in the project, returns the relative path to where
115
+ # the error occurred. Otherwise returns the absolute path to where the
116
+ # error occurred.
117
+ #
118
+ def path
119
+ @path ||= if project?
120
+ Pathname.new(
121
+ File.expand_path(project_backtrace_locations[0].absolute_path.to_s)
122
+ ).relative_path_from(
123
+ Pathname.new(Pakyow.config.root)
124
+ ).to_s
125
+ else
126
+ File.expand_path(project_backtrace_locations[0].absolute_path.to_s)
127
+ end
128
+ end
129
+
130
+ # Returns true if the error occurred in the project.
131
+ #
132
+ def project?
133
+ File.expand_path(backtrace[0].to_s).start_with?(Pakyow.config.root)
134
+ end
135
+
136
+ # Returns the backtrace without any of the framework locations, unless the
137
+ # error originated from the framework. Return value is as an array of
138
+ # strings rather than backtrace location objects.
139
+ #
140
+ def condensed_backtrace
141
+ if project?
142
+ project_backtrace_locations.map { |line|
143
+ line.to_s.gsub(/^#{Pakyow.config.root}\//, "")
144
+ }
145
+ else
146
+ padded_length = backtrace.map { |line|
147
+ Support::Dependencies.library_name(line).to_s.gsub(/^pakyow-/, "")
148
+ }.max_by(&:length).length + 3
149
+
150
+ backtrace.map { |line|
151
+ modified_line = Support::Dependencies.strip_path_prefix(line)
152
+ if line.start_with?(Pakyow.config.root)
153
+ "› ".rjust(padded_length) + modified_line
154
+ elsif modified_line.start_with?("ruby")
155
+ "ruby | ".rjust(padded_length) + modified_line.split("/", 3)[2].to_s
156
+ else
157
+ "#{Support::Dependencies.library_name(line).to_s.gsub(/^pakyow-/, "")} | ".rjust(padded_length) + modified_line.split("/", 2)[1].to_s
158
+ end
159
+ }
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def project_backtrace_locations
166
+ (cause || self).backtrace_locations.to_a.select { |line|
167
+ File.expand_path(line.absolute_path.to_s).start_with?(Pakyow.config.root)
168
+ }
169
+ end
170
+
171
+ def indent_as_code(message)
172
+ message.split("\n").map { |line|
173
+ " #{line}"
174
+ }.join("\n")
175
+ end
176
+
177
+ def indent_as_source(message, lineno)
178
+ message.split("\n").each_with_index.map { |line, i|
179
+ start = String.new(" #{lineno + i}|")
180
+ if i == 0
181
+ start << "›"
182
+ else
183
+ start << " "
184
+ end
185
+ "#{start} #{line}"
186
+ }.join("\n")
187
+ end
188
+
189
+ # @api private
190
+ class CLIFormatter
191
+ def initialize(error)
192
+ @error = error
193
+ end
194
+
195
+ def to_s
196
+ <<~MESSAGE
197
+ #{message}
198
+
199
+ #{Support::CLI.style.black.on_white.bold(" DETAILS ")}
200
+
201
+ #{details}
202
+
203
+ #{Support::CLI.style.black.on_white.bold(" BACKTRACE ")}
204
+
205
+ #{backtrace}
206
+ MESSAGE
207
+ end
208
+
209
+ def header
210
+ start = " #{@error.name.upcase} "
211
+
212
+ finish = if @error.project?
213
+ File.basename(@error.path)
214
+ else
215
+ ""
216
+ end
217
+
218
+ "#{start}#{" " * (80 - (start.length + finish.length + 2))} #{finish} "
219
+ end
220
+
221
+ def message
222
+ message = Support::CLI.style.white.on_red.bold(header)
223
+ message_lines = @error.message.split("\n")
224
+ if message_lines.any?
225
+ message << "\n\n#{self.class.indent(Support::CLI.style.red("›") + Support::CLI.style.bright_black(" #{self.class.format(message_lines.shift)}"))}\n"
226
+ message_lines.each do |line|
227
+ message << Support::CLI.style.bright_black("\n#{line}")
228
+ end
229
+ end
230
+
231
+ if @error.respond_to?(:contextual_message)
232
+ message = <<~MESSAGE
233
+ #{message}
234
+ #{Support::CLI.style.bright_black(self.class.indent(self.class.format(@error.contextual_message)))}
235
+ MESSAGE
236
+ end
237
+
238
+ message.rstrip
239
+ end
240
+
241
+ def details
242
+ Support::CLI.style.bright_black(self.class.indent(self.class.format(@error.details)))
243
+ end
244
+
245
+ def backtrace
246
+ @error.condensed_backtrace.map(&:to_s).join("\n")
247
+ end
248
+
249
+ private
250
+
251
+ class << self
252
+ # @api private
253
+ def indent(message)
254
+ message.split("\n").map { |line|
255
+ " #{line}"
256
+ }.join("\n")
257
+ end
258
+
259
+ # @api private
260
+ HIGHLIGHT_REGEX = /`([^']*)'/
261
+
262
+ # @api private
263
+ def format(message)
264
+ message.dup.tap do |message_to_format|
265
+ message.scan(HIGHLIGHT_REGEX).each do |match|
266
+ message_to_format.gsub!("`#{match[0]}'", Support::CLI.style.italic.blue(match[0]))
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end