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.
data/gems.rb CHANGED
@@ -4,26 +4,7 @@ source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  group :maintenance, optional: true do
7
- # [https://rubygems.org/gems/bake]
8
- # Bake is a Ruby library that helps you to create a new project.
9
- gem "bake"
10
- # [https://rubygems.org/gems/covered]
11
- # Covered is a Ruby library that helps you to test your project.
12
- gem "covered"
13
- # [https://rubygems.org/gems/bake-gem]
14
- # Bake Gem is a Bake extension that helps you to create a new Ruby
15
- # gem.
16
- gem "bake-gem"
17
- # [https://rubygems.org/gems/bake-modernize]
18
- # Bake Modernize is a Bake extension that helps you to modernize your
19
- # Ruby code.
20
- gem "bake-modernize"
21
- # [https://rubygems.org/gems/bake-github-pages]
22
- # Bake Github Pages is a Bake extension that helps you to publish your
23
- # project documentation to Github Pages.
24
- gem "bake-github-pages"
25
- # [https://rubygems.org/gems/utpia-project]
26
- # Utopia Project is a Bake extension that helps you to create a new
27
- # project.
28
- gem "utopia-project"
7
+ # Yard is a documentation generation tool for the Ruby programming language.
8
+ # [https://rubygems.org/gems/yard]
9
+ gem "yard"
29
10
  end
@@ -28,7 +28,7 @@ Create a new file named `config.ru`:
28
28
  require 'lennarb'
29
29
 
30
30
  MyApp = Lennarb::App.new do
31
- routes
31
+ routes do
32
32
  get '/' do |req, res|
33
33
  res.status = 200
34
34
  res.html('<h1>Welcome to Lennarb!</h1>')
@@ -37,7 +37,7 @@ MyApp = Lennarb::App.new do
37
37
  end
38
38
 
39
39
  MyApp.initialize!
40
- run App
40
+ run MyApp
41
41
  ```
42
42
 
43
43
  Start the server:
@@ -71,7 +71,7 @@ app.get '/html' do |req, res|
71
71
  end
72
72
 
73
73
  app.get '/json' do |req, res|
74
- res.json('{"message": "JSON response"}')
74
+ res.json({ message: "JSON response" })
75
75
  end
76
76
  ```
77
77
 
@@ -94,7 +94,7 @@ Lennarb::App.new do
94
94
 
95
95
  get '/users/:id' do |req, res|
96
96
  user_id = req.params[:id]
97
- res.json("{\"id\": #{user_id}}")
97
+ res.json({ id: user_id })
98
98
  end
99
99
  end
100
100
  end
@@ -124,7 +124,7 @@ Lennarb is thread-safe by design:
124
124
  ### Initialization
125
125
 
126
126
  ```ruby
127
- Myapp = Lennarb::App.new do
127
+ MyApp = Lennarb::App.new do
128
128
  # Define routes
129
129
  routes do
130
130
  end
@@ -189,7 +189,7 @@ Default error responses:
189
189
  ```ruby
190
190
  get '/api' do |req, res|
191
191
  res.status = 200
192
- res.json('{"status": "ok"}')
192
+ res.json({ status: "ok" })
193
193
  end
194
194
  ```
195
195
 
@@ -200,7 +200,7 @@ Default error responses:
200
200
  res.html('<h1>Web Page</h1>')
201
201
 
202
202
  # JSON for APIs
203
- res.json('{"data": "value"}')
203
+ res.json({ data: "value" })
204
204
 
205
205
  # Text for simple responses
206
206
  res.text('Hello')
@@ -213,3 +213,7 @@ For help and bug reports, please visit:
213
213
  - GitHub Issues: [lennarb/issues](https://github.com/aristotelesbr/lennarb/issues)
214
214
 
215
215
  Now you can run your app!
216
+
217
+ ```
218
+
219
+ ```
@@ -0,0 +1,38 @@
1
+ ## Mounting Applications
2
+
3
+ You can mount other applications at specific paths using the `mount` method. The component to be mounted must be a subclass of `Lennarb::App`.
4
+
5
+ ```ruby
6
+ class Blog < Lennarb::App
7
+ get "/" do |req, res|
8
+ res.html("<h1>Welcome to my blog</h1>")
9
+ end
10
+ end
11
+
12
+ class Admin < Lennarb::App
13
+ get "/" do |req, res|
14
+ res.html("<h1>Admin Dashboard</h1>")
15
+ end
16
+ end
17
+
18
+ class Application < Lennarb::Base
19
+ mount(Blog, at: "/blog")
20
+ mount(Admin, at: "/admin")
21
+ end
22
+ ```
23
+
24
+ ## Middleware Configuration
25
+
26
+ Middleware can be configured at both the base application level and within individual mounted applications. This allows for flexible and modular application design.
27
+
28
+ ```ruby
29
+ class Blog < Lennarb::App
30
+ middleware do
31
+ use MyCustomMiddleware
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## Conclusion
37
+
38
+ By using the `Base` class and the `mount` method, Lennarb provides a powerful way to structure your applications into modular components, each with its own routes and middleware.
data/lennarb.gemspec CHANGED
@@ -31,10 +31,11 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency "bigdecimal"
32
32
  spec.add_dependency "colorize", "~> 1.1"
33
33
  spec.add_dependency "rack", "~> 3.1"
34
- spec.add_dependency "superconfig"
34
+ spec.add_dependency "superconfig", "~> 3.0"
35
+ spec.add_dependency "logger", "~> 1.7"
35
36
  spec.add_development_dependency "bundler"
36
- spec.add_development_dependency "covered"
37
37
  spec.add_development_dependency "simplecov"
38
+ spec.add_development_dependency "simplecov-json"
38
39
  spec.add_development_dependency "minitest"
39
40
  spec.add_development_dependency "minitest-utils"
40
41
  spec.add_development_dependency "rack-test"
@@ -42,6 +43,5 @@ Gem::Specification.new do |spec|
42
43
  spec.add_development_dependency "standard"
43
44
  spec.add_development_dependency "standard-custom"
44
45
  spec.add_development_dependency "standard-performance"
45
- spec.add_development_dependency "m"
46
- spec.add_development_dependency "debug"
46
+ spec.add_development_dependency "debug" if RUBY_ENGINE == "ruby"
47
47
  end
data/lib/lennarb/app.rb CHANGED
@@ -1,171 +1,252 @@
1
1
  module Lennarb
2
+ # Main application class with hooks and helpers support
2
3
  class App
3
- # This error is raised whenever the app is initialized more than once.
4
- AlreadyInitializedError = Class.new(StandardError)
4
+ class << self
5
+ attr_writer :app
6
+ # Rack environment variable name
7
+ #
8
+ # @return [String] The environment variable name
9
+ def app
10
+ @app ||= self
11
+ end
5
12
 
6
- # The root app directory of the app.
7
- #
8
- # @returns [Pathname]
9
- #
10
- attr_accessor :root
13
+ # Define helper methods for the application
14
+ #
15
+ # @yield Block containing helper definitions
16
+ # @return [Module] The helpers module
17
+ def helpers(mod_or_block = nil, &block)
18
+ Helpers.define(self, mod_or_block, &block)
19
+ end
11
20
 
12
- # The current environment. Defaults to "development".
13
- # It can be set using the following environment variables:
14
- #
15
- # - `LENNA_ENV`
16
- # - `APP_ENV`
17
- # - `RACK_ENV`
18
- #
19
- # @returns [Lennarb::Environment]
20
- #
21
- attr_reader :env
21
+ # Define a before hook
22
+ #
23
+ # @yield [req, res] Block to execute before route
24
+ # @return [Array] The before hooks array
25
+ def before(&block)
26
+ Hooks.add(self, :before, &block)
27
+ end
22
28
 
23
- def initialize(&)
24
- @initialized = false
25
- self.root = Pathname.pwd
26
- self.env = compute_env
27
- instance_eval(&) if block_given?
28
- end
29
+ # Define an after hook
30
+ #
31
+ # @yield [req, res] Block to execute after route
32
+ # @return [Array] The after hooks array
33
+ def after(&block)
34
+ Hooks.add(self, :after, &block)
35
+ end
29
36
 
30
- # Set the current environment. See {Lennarb::Environment} for more details.
31
- #
32
- # @parameter [Hash] env
33
- #
34
- def env=(env)
35
- raise AlreadyInitializedError if initialized?
37
+ # Get routes for this app class
38
+ #
39
+ # @return [Routes] The routes instance
40
+ def routes
41
+ @routes ||= Routes.new
42
+ end
36
43
 
37
- @env = Environment.new(env)
38
- end
44
+ # Set up subclass
45
+ #
46
+ # @param [Class] subclass The new subclass
47
+ # @return [void]
48
+ def inherited(subclass)
49
+ super
50
+ # Each subclass gets its own routes
51
+ subclass.instance_variable_set(:@routes, Routes.new)
52
+ end
39
53
 
40
- # Mount an app at a specific path.
41
- #
42
- # @parameter [Object] The controller|app to mount.
43
- #
44
- # @returns [void]
45
- #
46
- # @example
47
- #
48
- # class PostController
49
- # extend Lennarb::Routes::Mixin
50
- #
51
- # get "/post/:id" do |req, res|
52
- # res.text("Post ##{req.params[:id]}")
53
- # end
54
- # end
55
- #
56
- # MyApp = Lennarb::App.new do
57
- # routes do
58
- # mount PostController
59
- # end
60
- #
61
- def mount(*controllers)
62
- controllers.each do |controller|
63
- raise ArgumentError, "Controller must respond to :routes" unless controller.respond_to?(:routes)
54
+ # Define route methods for all HTTP methods
55
+ HTTP_METHODS.each do |http_method|
56
+ define_method(http_method.downcase) do |path, &block|
57
+ routes.send(http_method.downcase, path, &block)
58
+ end
59
+ end
64
60
 
65
- self.controllers << controller
61
+ # Define root route (GET /)
62
+ #
63
+ # @yield [req, res, params] Route block
64
+ # @return [void]
65
+ def root(&block)
66
+ get("/", &block)
67
+ end
68
+
69
+ # Define configuration
70
+ #
71
+ # @param [Array<Symbol>] envs Environments
72
+ # @yield Configuration block
73
+ # @return [Config] The config instance
74
+ def config(*envs, &)
75
+ @config ||= Config.new(self)
76
+
77
+ if block_given?
78
+ write = envs.empty? || envs.map(&:to_sym).include?(env.name)
79
+ @config.instance_eval(&) if write
80
+ end
81
+
82
+ @config
66
83
  end
67
84
  end
68
85
 
69
- # Define the app's configuration. See {Lennarb::Config}.
70
- #
71
- # @returns [Lennarb::Config]
72
- #
73
- # @example Run config on every environment
74
- # app.config do
75
- # mandatory :database_url, string
76
- # end
77
- #
78
- # @example Run config on every a specific environment
79
- # app.config :development do
80
- # set :domain, "example.dev"
81
- # end
82
- #
83
- # @example Run config on every a specific environment
84
- # app.config :development, :test do
85
- # set :domain, "example.dev"
86
- # end
86
+ # Instance methods
87
+
88
+ # Initialize a new app
87
89
  #
88
- def config(*envs, &)
89
- @config ||= Config.new
90
+ # @yield [self] Configuration block
91
+ def initialize(&block)
92
+ @initialized = false
93
+ @root = Pathname.pwd
94
+ @env = Environment.new(compute_env)
90
95
 
91
- write = block_given? &&
92
- (envs.map(&:to_sym).include?(env.to_sym) || envs.empty?)
96
+ instance_eval(&block) if block_given?
97
+ end
93
98
 
94
- @config.instance_eval(&) if write
99
+ # The current environment
100
+ attr_reader :env
95
101
 
96
- @config
97
- end
102
+ # The root directory
103
+ attr_accessor :root
98
104
 
99
- # Define the app's route. See {Lennarb::RouteNode} for more details.
105
+ # Set environment
100
106
  #
101
- # @returns [Lennarb::RouteNode]
102
- #
103
- def routes(&)
104
- @routes ||= Routes.new
105
- @routes.instance_eval(&) if block_given?
106
- @routes
107
+ # @param [String, Symbol] value Environment name
108
+ # @raise [AlreadyInitializedError] If already initialized
109
+ # @return [Environment] The new environment
110
+ def env=(value)
111
+ raise AlreadyInitializedError if initialized?
112
+ @env = Environment.new(value)
107
113
  end
108
114
 
109
- # The Rack app.
115
+ # Get the Rack app with middleware
110
116
  #
117
+ # @return [#call] The Rack app
111
118
  def app
112
119
  @app ||= begin
113
- request_handler = RequestHandler.new(self)
120
+ handler = build_request_handler
121
+ stack = middleware.to_a
114
122
 
115
123
  Rack::Builder.app do
116
- run request_handler
124
+ stack.each { |middleware, args, block| use(middleware, *args, &block) }
125
+ run handler
117
126
  end
118
127
  end
119
128
  end
120
129
 
121
- # Store mounted app's
130
+ # Rack interface method
122
131
  #
123
- def controllers
124
- @controllers ||= []
132
+ # @param [Hash] env Rack environment
133
+ # @return [Array] Rack response
134
+ def call(env)
135
+ env[RACK_LENNA_APP] = self
136
+ app.call(env)
125
137
  end
126
- alias_method :mounted_apps, :controllers
127
138
 
128
- # Check if the app is initialized.
139
+ # Define middleware
129
140
  #
130
- # @returns [Boolean]
141
+ # @yield Block to configure middleware
142
+ # @return [MiddlewareStack] The middleware stack
143
+ def middleware(&block)
144
+ @middleware_stack ||= default_middleware_stack
145
+ @middleware_stack.instance_eval(&block) if block_given?
146
+ @middleware_stack
147
+ end
148
+
149
+ # Define helpers (instance method)
131
150
  #
132
- def initialized? = @initialized
151
+ # @yield Block with helper definitions
152
+ # @return [Module] The helpers module
153
+ def helpers(&block)
154
+ self.class.helpers(&block)
155
+ end
133
156
 
134
- # Initialize the app.
157
+ # Define before hook (instance method)
135
158
  #
136
- # @returns [void]
159
+ # @yield [req, res] Before hook block
160
+ # @return [Array] The before hooks array
161
+ def before(&block)
162
+ self.class.before(&block)
163
+ end
164
+
165
+ # Define after hook (instance method)
137
166
  #
138
- def initialize!
139
- raise AlreadyInitializedError if initialized?
167
+ # @yield [req, res] After hook block
168
+ # @return [Array] The after hooks array
169
+ def after(&block)
170
+ self.class.after(&block)
171
+ end
140
172
 
141
- controllers.each do
142
- routes.store.merge!(it.routes.store)
173
+ # Get/define routes
174
+ #
175
+ # @yield Block to define routes
176
+ # @return [Routes] The routes instance
177
+ def routes(&block)
178
+ if block_given?
179
+ self.class.instance_exec(&block)
143
180
  end
144
181
 
145
- @initialized = true
146
-
147
- app.freeze
148
- routes.freeze
182
+ self.class.routes
149
183
  end
150
184
 
151
- # Call the app.
185
+ # Get/define configuration
152
186
  #
153
- # @parameter [Hash] env
187
+ # @param [Array<Symbol>] envs Environments
188
+ # @yield Configuration block
189
+ # @return [Config] The config instance
190
+ def config(*envs, &)
191
+ @config ||= self.class.config
192
+
193
+ if block_given?
194
+ write = envs.empty? || envs.map(&:to_sym).include?(env.name)
195
+ @config.instance_eval(&) if write
196
+ end
197
+
198
+ @config
199
+ end
200
+
201
+ # Initialize the app
154
202
  #
155
- def call(env)
156
- env[RACK_LENNA_APP] = self
157
- Dir.chdir(root) { return app.call(env) }
203
+ # @return [self] The initialized app
204
+ # @raise [AlreadyInitializedError] If already initialized
205
+ def initialize!
206
+ raise AlreadyInitializedError if @initialized
207
+
208
+ @initialized = true
209
+ routes.freeze
210
+ self
158
211
  end
159
212
 
160
- # Compute the current environment.
213
+ # Check if initialized
161
214
  #
162
- # @returns [String]
215
+ # @return [Boolean] true if initialized
216
+ def initialized?
217
+ @initialized
218
+ end
219
+
220
+ # Error for already initialized app
221
+ AlreadyInitializedError = Class.new(StandardError)
222
+
223
+ protected
224
+
225
+ # Build the request handler
163
226
  #
164
- # @private
227
+ # @return [RequestHandler] The request handler
228
+ def build_request_handler
229
+ RequestHandler.new(self)
230
+ end
231
+
232
+ # Create default middleware stack
233
+ #
234
+ # @return [MiddlewareStack] The middleware stack
235
+ private def default_middleware_stack
236
+ stack = MiddlewareStack.new
237
+ stack.use(Lennarb::Middleware::RequestLogger)
238
+ stack.use(Rack::Runtime)
239
+ stack.use(Rack::Head)
240
+ stack.use(Rack::ETag)
241
+ stack.use(Rack::ShowExceptions) if env.development?
242
+ stack
243
+ end
244
+
245
+ # Compute environment from ENV variables
165
246
  #
247
+ # @return [String] Environment name
166
248
  private def compute_env
167
- env = ENV_NAMES.map { ENV[_1] }.compact.first.to_s
168
-
249
+ env = ENV_NAMES.map { |name| ENV[name] }.compact.first.to_s
169
250
  env.empty? ? "development" : env
170
251
  end
171
252
  end