lennarb 1.4.1 → 1.5.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.
- checksums.yaml +4 -4
- data/.github/workflows/coverage.yaml +11 -33
- data/.github/workflows/documentation.yaml +32 -13
- data/.gitignore +9 -0
- data/.yardopts +8 -0
- data/CODE_OF_CONDUCT.md +118 -0
- data/CONTRIBUTING.md +155 -0
- data/README.pt-BR.md +147 -0
- data/Rakefile +32 -1
- data/changelog.md +38 -0
- data/gems.rb +3 -22
- data/guides/getting-started/readme.md +11 -7
- data/guides/mounting-applications/readme.md +38 -0
- data/lennarb.gemspec +4 -4
- data/lib/lennarb/app.rb +199 -118
- data/lib/lennarb/base.rb +314 -0
- data/lib/lennarb/config.rb +23 -5
- data/lib/lennarb/constants.rb +12 -0
- data/lib/lennarb/environment.rb +11 -7
- data/lib/lennarb/errors.rb +17 -0
- data/lib/lennarb/helpers.rb +40 -0
- data/lib/lennarb/hooks.rb +71 -0
- data/lib/lennarb/logger.rb +100 -0
- data/lib/lennarb/middleware/request_logger.rb +117 -0
- data/lib/lennarb/middleware_stack.rb +56 -0
- data/lib/lennarb/parameter_filter.rb +76 -0
- data/lib/lennarb/request.rb +51 -41
- data/lib/lennarb/request_handler.rb +39 -9
- data/lib/lennarb/response.rb +23 -21
- data/lib/lennarb/route_node.rb +17 -5
- data/lib/lennarb/routes.rb +43 -47
- data/lib/lennarb/version.rb +2 -2
- data/lib/lennarb.rb +22 -2
- data/logo/lennarb.svg +11 -0
- data/readme.md +176 -35
- metadata +36 -23
- data/lib/lennarb/constansts.rb +0 -6
- data/logo/lennarb.png +0 -0
data/lib/lennarb/base.rb
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
module Lennarb
|
2
|
+
# Base class for mounting applications with middleware support.
|
3
|
+
# This class serves as a proxy for mounting Lennarb::App instances,
|
4
|
+
# providing a lightweight router to dispatch requests to the appropriate app.
|
5
|
+
#
|
6
|
+
# @example Creating a mounting application
|
7
|
+
# class Application < Lennarb::Base
|
8
|
+
# # Define base-level middleware
|
9
|
+
# middleware do
|
10
|
+
# use Rack::Session::Cookie, secret: "your_secret"
|
11
|
+
# use Rack::Protection
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # Mount other applications
|
15
|
+
# mount(Blog, at: "/blog")
|
16
|
+
# mount(Admin, at: "/admin")
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # Start the server
|
20
|
+
# run Application.new
|
21
|
+
#
|
22
|
+
# @since 1.4.0
|
23
|
+
class Base
|
24
|
+
# This error is raised whenever the app is initialized more than once.
|
25
|
+
# @since 1.0.0
|
26
|
+
AlreadyInitializedError = Class.new(StandardError)
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# Store for mounted applications at class level
|
30
|
+
# @return [Hash] mounted applications by path
|
31
|
+
# @since 1.4.0
|
32
|
+
def mounted_apps
|
33
|
+
@mounted_apps ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Define the app's middleware stack at class level.
|
37
|
+
# This allows defining middleware that will be applied before
|
38
|
+
# routing to any mounted applications.
|
39
|
+
#
|
40
|
+
# @yield [middleware] Block to configure middleware
|
41
|
+
# @return [Lennarb::MiddlewareStack] the middleware stack
|
42
|
+
# @since 1.4.0
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# middleware do
|
46
|
+
# use Rack::Session::Cookie, secret: "your_secret"
|
47
|
+
# use Rack::Protection
|
48
|
+
# end
|
49
|
+
def middleware(&block)
|
50
|
+
@middleware ||= MiddlewareStack.new
|
51
|
+
@middleware.instance_eval(&block) if block_given?
|
52
|
+
@middleware
|
53
|
+
end
|
54
|
+
|
55
|
+
# Define the app's configuration at class level
|
56
|
+
# @param [Array<Symbol>] envs Environments to apply the configuration to
|
57
|
+
# @yield [config] Block to configure the application
|
58
|
+
# @return [Lennarb::Config] the configuration
|
59
|
+
# @since 1.4.0
|
60
|
+
#
|
61
|
+
# @example Configure for all environments
|
62
|
+
# config do
|
63
|
+
# set :title, "My Application"
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# @example Configure for specific environments
|
67
|
+
# config :development, :test do
|
68
|
+
# set :debug, true
|
69
|
+
# end
|
70
|
+
def config(*envs, &block)
|
71
|
+
@config ||= Config.new
|
72
|
+
@config.instance_eval(&block) if block_given?
|
73
|
+
@config
|
74
|
+
end
|
75
|
+
|
76
|
+
# Mount a component at the given path.
|
77
|
+
# @param [Class] component The component to mount (must be a Lennarb::App subclass)
|
78
|
+
# @param [Hash] options The mounting options
|
79
|
+
# @option options [String] :at The path to mount the component at (defaults to "/")
|
80
|
+
# @raise [ArgumentError] If the component is not a Lennarb::App subclass
|
81
|
+
# @return [void]
|
82
|
+
# @since 1.4.0
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# mount(Blog, at: "/blog")
|
86
|
+
# mount(Admin, at: "/admin")
|
87
|
+
def mount(component, at: nil)
|
88
|
+
if component.is_a?(Class) && component < Lennarb::App
|
89
|
+
path = normalize_mount_path(at || "/")
|
90
|
+
mounted_apps[path] = component
|
91
|
+
else
|
92
|
+
raise ArgumentError, "Component must be a Lennarb::App subclass"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Normalize the mount path.
|
97
|
+
# @param [String] path The path to normalize
|
98
|
+
# @return [String] The normalized path
|
99
|
+
# @since 1.4.0
|
100
|
+
# @api private
|
101
|
+
private def normalize_mount_path(path)
|
102
|
+
path = "/#{path}" unless path.start_with?("/")
|
103
|
+
path = path[0..-2] if path.end_with?("/") && path != "/"
|
104
|
+
path
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# The current environment.
|
109
|
+
# @return [Lennarb::Environment] The environment
|
110
|
+
# @since 1.0.0
|
111
|
+
attr_reader :env
|
112
|
+
|
113
|
+
# The root directory of the application.
|
114
|
+
# @return [Pathname] The root directory
|
115
|
+
# @since 1.0.0
|
116
|
+
attr_accessor :root
|
117
|
+
|
118
|
+
# Get the mounted applications.
|
119
|
+
# @return [Hash] The mounted applications by path
|
120
|
+
# @since 1.4.0
|
121
|
+
attr_reader :mounted_apps
|
122
|
+
|
123
|
+
# Initialize a new Base instance.
|
124
|
+
# @yield [self] Block to configure the application
|
125
|
+
# @return [Base] The initialized application
|
126
|
+
# @since 1.0.0
|
127
|
+
def initialize(&block)
|
128
|
+
@initialized = false
|
129
|
+
@mounted_apps = {}
|
130
|
+
@middleware = nil
|
131
|
+
self.root = Pathname.pwd
|
132
|
+
@env = Environment.new(compute_env)
|
133
|
+
|
134
|
+
# Initialize mounted applications from class definition
|
135
|
+
self.class.mounted_apps.each do |path, app_class|
|
136
|
+
@mounted_apps[path] = app_class
|
137
|
+
end
|
138
|
+
|
139
|
+
instance_eval(&block) if block_given?
|
140
|
+
end
|
141
|
+
|
142
|
+
# Define the app's middleware stack.
|
143
|
+
# Middleware defined here will be applied to all requests before
|
144
|
+
# they are routed to mounted applications.
|
145
|
+
#
|
146
|
+
# @yield [middleware] Block to configure middleware
|
147
|
+
# @return [Lennarb::MiddlewareStack] the middleware stack
|
148
|
+
# @since 1.4.0
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# middleware do
|
152
|
+
# use Rack::Session::Cookie, secret: "your_secret"
|
153
|
+
# use Rack::Protection
|
154
|
+
# end
|
155
|
+
def middleware(&block)
|
156
|
+
@middleware ||= MiddlewareStack.new
|
157
|
+
@middleware.instance_eval(&block) if block_given?
|
158
|
+
@middleware
|
159
|
+
end
|
160
|
+
|
161
|
+
# Define the app's configuration.
|
162
|
+
# @param [Array<Symbol>] envs Environments to apply the configuration to
|
163
|
+
# @yield [config] Block to configure the application
|
164
|
+
# @return [Lennarb::Config] the configuration
|
165
|
+
# @since 1.4.0
|
166
|
+
def config(*envs, &block)
|
167
|
+
@config ||= Config.new
|
168
|
+
|
169
|
+
write = block_given? &&
|
170
|
+
(envs.map(&:to_sym).include?(env.to_sym) || envs.empty?)
|
171
|
+
|
172
|
+
@config.instance_eval(&block) if write
|
173
|
+
|
174
|
+
@config
|
175
|
+
end
|
176
|
+
|
177
|
+
# Check if the app is initialized.
|
178
|
+
# @return [Boolean] true if initialized, false otherwise
|
179
|
+
# @since 1.4.0
|
180
|
+
def initialized?
|
181
|
+
@initialized
|
182
|
+
end
|
183
|
+
|
184
|
+
# Initialize the app.
|
185
|
+
# @return [self] The initialized app
|
186
|
+
# @raise [AlreadyInitializedError] If the app is already initialized
|
187
|
+
# @since 1.4.0
|
188
|
+
def initialize!
|
189
|
+
raise AlreadyInitializedError if initialized?
|
190
|
+
@initialized = true
|
191
|
+
self
|
192
|
+
end
|
193
|
+
|
194
|
+
# Set the environment for the application.
|
195
|
+
# @param [String, Symbol] value The environment name
|
196
|
+
# @raise [AlreadyInitializedError] If the app is already initialized
|
197
|
+
# @return [Lennarb::Environment] The new environment
|
198
|
+
# @since 1.4.0
|
199
|
+
def env=(value)
|
200
|
+
raise AlreadyInitializedError if initialized?
|
201
|
+
@env = Environment.new(value)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Freeze the app.
|
205
|
+
# @return [void]
|
206
|
+
# @since 1.0.0
|
207
|
+
def freeze!
|
208
|
+
app.freeze
|
209
|
+
end
|
210
|
+
|
211
|
+
# The Rack app with all middlewares and mounted applications.
|
212
|
+
# This builds a middleware stack around the URL map of mounted applications.
|
213
|
+
#
|
214
|
+
# @return [#call] The Rack application
|
215
|
+
# @since 1.0.0
|
216
|
+
def app
|
217
|
+
@app ||= begin
|
218
|
+
url_map = build_url_map
|
219
|
+
|
220
|
+
stack = middleware.to_a
|
221
|
+
|
222
|
+
Rack::Builder.app do
|
223
|
+
stack.each { |middleware, args, block| use(middleware, *args, &block) }
|
224
|
+
run url_map
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Call the app - main Rack entry point.
|
230
|
+
# This method is called by Rack when a request is received.
|
231
|
+
#
|
232
|
+
# @param [Hash] env The Rack environment
|
233
|
+
# @return [Array(Integer, Hash, #each)] The Rack response
|
234
|
+
# @since 1.0.0
|
235
|
+
def call(env)
|
236
|
+
# Store reference to the current app in the env
|
237
|
+
env[RACK_LENNA_APP] = self
|
238
|
+
|
239
|
+
# Call the app with middlewares
|
240
|
+
app.call(env)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Mount a component at the given path (instance method)
|
244
|
+
# @param [Class] component The component to mount (must be a Lennarb::App subclass)
|
245
|
+
# @param [Hash] options The mounting options
|
246
|
+
# @option options [String] :at The path to mount the component at (defaults to "/")
|
247
|
+
# @raise [ArgumentError] If the component is not a Lennarb::App subclass
|
248
|
+
# @return [void]
|
249
|
+
# @since 1.4.0
|
250
|
+
#
|
251
|
+
# @example
|
252
|
+
# mount(Blog, at: "/blog")
|
253
|
+
def mount(component, at: nil)
|
254
|
+
if component.is_a?(Class) && component < Lennarb::App
|
255
|
+
path = normalize_mount_path(at || "/")
|
256
|
+
@mounted_apps[path] = component
|
257
|
+
else
|
258
|
+
raise ArgumentError, "Component must be a Lennarb::App subclass"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
# Build the URL map with all mounted applications.
|
265
|
+
# @return [Rack::URLMap] The URL map
|
266
|
+
# @since 1.4.0
|
267
|
+
# @api private
|
268
|
+
def build_url_map
|
269
|
+
url_map = {}
|
270
|
+
|
271
|
+
if mounted_apps.any?
|
272
|
+
mounted_apps.each do |path, app_class|
|
273
|
+
app_instance = app_class.new
|
274
|
+
app_instance.initialize!
|
275
|
+
url_map[path] = app_instance
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# If no app is mounted at root path, provide a default handler
|
280
|
+
url_map["/"] = default_app_handler unless url_map.key?("/")
|
281
|
+
|
282
|
+
Rack::URLMap.new(url_map)
|
283
|
+
end
|
284
|
+
|
285
|
+
# Default handler for when no app is mounted at root path.
|
286
|
+
# @return [#call] A simple not found handler
|
287
|
+
# @since 1.4.0
|
288
|
+
# @api private
|
289
|
+
def default_app_handler
|
290
|
+
# config.logger.warn("No application mounted at root path. Default handler will be used.")
|
291
|
+
->(env) { [404, {"content-type" => "text/plain"}, ["Not Found"]] }
|
292
|
+
end
|
293
|
+
|
294
|
+
# Normalize the mount path.
|
295
|
+
# @param [String] path The path to normalize
|
296
|
+
# @return [String] The normalized path
|
297
|
+
# @since 1.4.0
|
298
|
+
# @api private
|
299
|
+
def normalize_mount_path(path)
|
300
|
+
path = "/#{path}" unless path.start_with?("/")
|
301
|
+
path = path[0..-2] if path.end_with?("/") && path != "/"
|
302
|
+
path
|
303
|
+
end
|
304
|
+
|
305
|
+
# Compute the current environment from environment variables.
|
306
|
+
# @return [String] The environment name
|
307
|
+
# @since 1.4.0
|
308
|
+
# @api private
|
309
|
+
def compute_env
|
310
|
+
env = ENV_NAMES.map { |name| ENV[name] }.compact.first.to_s
|
311
|
+
env.empty? ? "development" : env
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
data/lib/lennarb/config.rb
CHANGED
@@ -3,14 +3,22 @@ module Lennarb
|
|
3
3
|
# It uses {https://rubygems.org/gems/superconfig SuperConfig} to define the
|
4
4
|
# configuration.
|
5
5
|
class Config < SuperConfig::Base
|
6
|
-
MissingEnvironmentVariable = Class.new(StandardError)
|
7
|
-
MissingCallable = Class.new(StandardError)
|
8
|
-
|
9
6
|
undef_method :credential
|
10
7
|
|
11
|
-
|
8
|
+
attr_reader :app
|
9
|
+
attr_accessor :stderr_output
|
10
|
+
|
11
|
+
def initialize(app = nil, silent: !ENV["LENNARB_SILENT_LOGS"].nil?, **options)
|
12
|
+
self.stderr_output = if silent
|
13
|
+
nil
|
14
|
+
else
|
15
|
+
$stderr
|
16
|
+
end
|
17
|
+
|
18
|
+
@app = app
|
12
19
|
block = proc { true }
|
13
|
-
super(
|
20
|
+
super(**options, &block)
|
21
|
+
apply_defaults_settings
|
14
22
|
end
|
15
23
|
|
16
24
|
# @private
|
@@ -30,5 +38,15 @@ module Lennarb
|
|
30
38
|
raise MissingCallable,
|
31
39
|
"arg[1] must respond to #call or a block must be provided"
|
32
40
|
end
|
41
|
+
|
42
|
+
private def apply_defaults_settings
|
43
|
+
set :logger,
|
44
|
+
Lennarb::Logger.new(
|
45
|
+
::Logger.new(stderr_output),
|
46
|
+
colorize: true,
|
47
|
+
tag: :lennarb
|
48
|
+
)
|
49
|
+
set :enable_reloading, false
|
50
|
+
end
|
33
51
|
end
|
34
52
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Lennarb
|
2
|
+
# The root app directory of the app.
|
3
|
+
RACK_LENNA_APP = "lennarb.app"
|
4
|
+
# The current environment. Defaults to "development".
|
5
|
+
ENV_NAMES = %w[LENNA_ENV APP_ENV RACK_ENV]
|
6
|
+
# The HTTP methods.
|
7
|
+
HTTP_METHODS = %i[GET POST PUT PATCH DELETE HEAD OPTIONS]
|
8
|
+
# The HTTP status codes.
|
9
|
+
CONTENT_TYPE = {HTML: "text/html", TEXT: "text/plain", JSON: "application/json"}
|
10
|
+
# The current environment. Defaults to "development".
|
11
|
+
NAMES = %i[development test production local]
|
12
|
+
end
|
data/lib/lennarb/environment.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
module Lennarb
|
2
|
+
# Manage the environment of the application.
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# app.env.development? # => true
|
6
|
+
# app.env.test? # => false
|
7
|
+
#
|
2
8
|
class Environment
|
3
|
-
NAMES = %i[development test production local]
|
4
|
-
|
5
9
|
# Returns the name of the environment.
|
6
|
-
# @
|
10
|
+
# @param name [Symbol]
|
7
11
|
#
|
8
12
|
attr_reader :name
|
9
13
|
|
10
14
|
# Initialize the environment.
|
11
|
-
# @
|
15
|
+
# @param name [String, Symbol] The name of the environment.
|
12
16
|
#
|
13
17
|
def initialize(name)
|
14
18
|
@name = name.to_sym
|
@@ -42,17 +46,17 @@ module Lennarb
|
|
42
46
|
alias_method :===, :==
|
43
47
|
|
44
48
|
# Returns the name of the environment as a symbol.
|
45
|
-
# @
|
49
|
+
# @retrn [Symbol]
|
46
50
|
#
|
47
51
|
def to_sym = name
|
48
52
|
|
49
53
|
# Returns the name of the environment as a string.
|
50
|
-
# @
|
54
|
+
# @retrn [String]
|
51
55
|
#
|
52
56
|
def to_s = name.to_s
|
53
57
|
|
54
58
|
# Returns the name of the environment as a string.
|
55
|
-
# @
|
59
|
+
# @retrn [String]
|
56
60
|
def inspect = to_s.inspect
|
57
61
|
|
58
62
|
# Yields a block if the environment is the same as the given environment.
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Lennarb
|
2
|
+
# Lite implementation of app.
|
3
|
+
#
|
4
|
+
Error = Class.new(StandardError)
|
5
|
+
# This error is raised whenever the app is initialized more than once.
|
6
|
+
#
|
7
|
+
DuplicateRouteError = Class.new(StandardError)
|
8
|
+
# This error is raised whenever the app is initialized more than once.
|
9
|
+
#
|
10
|
+
MissingEnvironmentVariable = Class.new(StandardError)
|
11
|
+
# This error is raised whenever the app is initialized more than once.
|
12
|
+
#
|
13
|
+
MissingCallable = Class.new(StandardError)
|
14
|
+
# This error is raised whenever the app is initialized more than once.
|
15
|
+
#
|
16
|
+
RoutesFrozenError = Class.new(RuntimeError)
|
17
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Lennarb
|
2
|
+
# Simple helpers module for Lennarb applications.
|
3
|
+
# Provides helper methods to be used in routes and hooks.
|
4
|
+
module Helpers
|
5
|
+
# Store helpers for app classes
|
6
|
+
@app_helpers = {}
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Get the app helpers map
|
10
|
+
attr_reader :app_helpers
|
11
|
+
|
12
|
+
# Get helpers module for an app class
|
13
|
+
#
|
14
|
+
# @param [Class] app_class The application class
|
15
|
+
# @return [Module] The helpers module
|
16
|
+
def for(app_class)
|
17
|
+
app_helpers[app_class] ||= Module.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Define helpers for an app class
|
21
|
+
#
|
22
|
+
# @param [Class] app_class The application class
|
23
|
+
# @param [Module, Proc] mod_or_block The module to include or block with helper definitions
|
24
|
+
# @return [Module] The helpers module
|
25
|
+
def define(app_class, mod_or_block = nil, &block)
|
26
|
+
mod = self.for(app_class)
|
27
|
+
|
28
|
+
case mod_or_block
|
29
|
+
when Module
|
30
|
+
mod.include(mod_or_block)
|
31
|
+
when Proc
|
32
|
+
mod.module_eval(&mod_or_block)
|
33
|
+
end
|
34
|
+
|
35
|
+
mod.module_eval(&block) if block_given?
|
36
|
+
mod
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Lennarb
|
2
|
+
# Provides hook functionality for Lennarb applications.
|
3
|
+
# Hooks execute code before and after route handlers.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# Hooks.add(MyApp, :before) do |req, res|
|
7
|
+
# res.headers["X-My-Header"] = "MyValue"
|
8
|
+
# end
|
9
|
+
# Hooks.add(MyApp, :after) do |req, res|
|
10
|
+
# res.body << "Goodbye!"
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# @note
|
14
|
+
# - Hooks are executed in the order they are added.
|
15
|
+
# - The context of the hook is the object that calls the route handler.
|
16
|
+
# - Hooks can modify the request and response objects.
|
17
|
+
# - Hooks can be used to implement middleware-like functionality.
|
18
|
+
# - Hooks are not thread-safe. Use with caution in multi-threaded environments.
|
19
|
+
module Hooks
|
20
|
+
# Valid hook types
|
21
|
+
TYPES = [:before, :after].freeze
|
22
|
+
|
23
|
+
# Store hooks for each app class
|
24
|
+
@app_hooks = {}
|
25
|
+
|
26
|
+
class << self
|
27
|
+
# Get the hooks hash
|
28
|
+
#
|
29
|
+
# @return [Hash] The hooks hash with app classes as keys
|
30
|
+
attr_reader :app_hooks
|
31
|
+
|
32
|
+
# Get the hooks for an app class
|
33
|
+
#
|
34
|
+
# @param [Class] app_class The application class
|
35
|
+
# @return [Hash] The hooks hash with :before and :after keys
|
36
|
+
def for(app_class)
|
37
|
+
app_hooks[app_class] ||= {before: [], after: []}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add a hook for an app class
|
41
|
+
#
|
42
|
+
# @param [Class] app_class The application class
|
43
|
+
# @param [Symbol] type The hook type (:before or :after)
|
44
|
+
# @param [Proc] block The hook block
|
45
|
+
# @return [Array] The hooks array for the given type
|
46
|
+
def add(app_class, type, &block)
|
47
|
+
raise ArgumentError, "Invalid hook type: #{type}" unless TYPES.include?(type)
|
48
|
+
|
49
|
+
hooks = self.for(app_class)
|
50
|
+
hooks[type] << block if block_given?
|
51
|
+
hooks[type]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Execute hooks of a given type
|
55
|
+
#
|
56
|
+
# @param [Object] context The execution context
|
57
|
+
# @param [Class] app_class The application class
|
58
|
+
# @param [Symbol] type The hook type to execute
|
59
|
+
# @param [Request] req The request object
|
60
|
+
# @param [Response] res The response object
|
61
|
+
# @return [void]
|
62
|
+
def execute(context, app_class, type, req, res)
|
63
|
+
hooks = self.for(app_class)[type]
|
64
|
+
|
65
|
+
hooks.each do |hook|
|
66
|
+
context.instance_exec(req, res, &hook)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Lennarb
|
2
|
+
class Logger
|
3
|
+
# @api private
|
4
|
+
LEVELS = {
|
5
|
+
debug: ::Logger::DEBUG,
|
6
|
+
info: ::Logger::INFO,
|
7
|
+
warn: ::Logger::WARN,
|
8
|
+
error: ::Logger::ERROR,
|
9
|
+
fatal: ::Logger::FATAL
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
DEFAULT_FORMATTER = proc do |options|
|
14
|
+
line = [options[:tag], options[:message]].compact.join(" ")
|
15
|
+
"#{line}\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Initialize a new logger
|
19
|
+
#
|
20
|
+
# @param [::Logger] logger Logger instance to use
|
21
|
+
# @param [Proc] formatter Custom formatter for messages
|
22
|
+
# @param [Array, String, Symbol, nil] tag Tag to identify log messages
|
23
|
+
# @param [Symbol, nil] tag_color Color for the tag
|
24
|
+
# @param [Symbol, nil] message_color Color for the message
|
25
|
+
# @param [Boolean] colorize Force colorization even when not TTY
|
26
|
+
def initialize(
|
27
|
+
logger = ::Logger.new($stdout, level: ::Logger::INFO),
|
28
|
+
formatter: DEFAULT_FORMATTER,
|
29
|
+
tag: nil,
|
30
|
+
tag_color: nil,
|
31
|
+
message_color: nil,
|
32
|
+
colorize: false
|
33
|
+
)
|
34
|
+
@logger = logger.dup
|
35
|
+
@logger.formatter = proc { |*args| format_log(*args) }
|
36
|
+
@tag = Array(tag)
|
37
|
+
@formatter = formatter
|
38
|
+
@tag_color = tag_color
|
39
|
+
@message_color = message_color
|
40
|
+
@colorize = colorize || $stdout.tty?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create a new logger with additional tags
|
44
|
+
#
|
45
|
+
# @param [Array<Symbol, String>] tags Additional tags
|
46
|
+
# @yield [logger] Block to execute with the new logger
|
47
|
+
# @return [Logger] New instance with added tags
|
48
|
+
#
|
49
|
+
# @example With block
|
50
|
+
# logger.tagged(:api) { |l| l.info("message") }
|
51
|
+
#
|
52
|
+
# @example Without block
|
53
|
+
# api_logger = logger.tagged(:api)
|
54
|
+
# api_logger.info("message")
|
55
|
+
def tagged(*tags)
|
56
|
+
new_logger = Logger.new(
|
57
|
+
@logger,
|
58
|
+
formatter: @formatter,
|
59
|
+
tag: @tag.dup.concat(tags),
|
60
|
+
tag_color: @tag_color,
|
61
|
+
message_color: @message_color,
|
62
|
+
colorize: @colorize
|
63
|
+
)
|
64
|
+
|
65
|
+
yield new_logger if block_given?
|
66
|
+
|
67
|
+
new_logger
|
68
|
+
end
|
69
|
+
|
70
|
+
# Define methods for each log level
|
71
|
+
# debug, info, warn, error, fatal
|
72
|
+
LEVELS.each_key do |level|
|
73
|
+
define_method(level) do |message = nil, &block|
|
74
|
+
return self if @logger.level > LEVELS[level]
|
75
|
+
|
76
|
+
message = block.call if block && !message
|
77
|
+
@logger.add(LEVELS[level], message)
|
78
|
+
|
79
|
+
self
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Format the log message
|
86
|
+
def format_log(_severity, _time, _progname, message)
|
87
|
+
tag = @tag.map { |t| "[#{t}]" }.join(" ")
|
88
|
+
tag = colorize_text(tag, @tag_color)
|
89
|
+
message = colorize_text(message, @message_color)
|
90
|
+
|
91
|
+
@formatter.call(message: message, tag: tag.empty? ? nil : tag)
|
92
|
+
end
|
93
|
+
|
94
|
+
def colorize_text(text, color)
|
95
|
+
return text.to_s unless @colorize
|
96
|
+
return text.to_s if color.nil?
|
97
|
+
text.to_s.colorize(color)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|