leanweb 0.1.3 → 0.3.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/bin/leanweb +21 -16
- data/contrib/lib/hawese.rb +62 -0
- data/contrib/lib/lean_mail.rb +125 -0
- data/contrib/lib/leanweb/controller_mixins/render_with_layout.rb +170 -0
- data/contrib/lib/rack/session/json_file.rb +84 -0
- data/lib/leanweb/app.rb +18 -30
- data/lib/leanweb/controller.rb +56 -54
- data/lib/leanweb/helpers.rb +34 -0
- data/lib/leanweb/route.rb +90 -52
- data/lib/leanweb/version.rb +13 -0
- data/lib/leanweb.rb +13 -7
- metadata +118 -39
data/lib/leanweb/controller.rb
CHANGED
@@ -2,19 +2,20 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
10
|
require 'rack'
|
11
|
+
require 'tilt'
|
11
12
|
|
12
13
|
module LeanWeb
|
13
14
|
# Controller is a base controller with `@route`, `@request` and `@response`
|
14
|
-
# private attributes that will be shared with your views when you
|
15
|
-
#
|
15
|
+
# private attributes that will be shared with your views when you
|
16
|
+
# {#render_response}.
|
16
17
|
#
|
17
|
-
# Even if you don't {
|
18
|
+
# Even if you don't {#render_response}, you can use the `.finish` method from
|
18
19
|
# `Rack::Response` on `@response` to return a proper `Rack` response.
|
19
20
|
class Controller
|
20
21
|
# @param route [Route]
|
@@ -23,67 +24,68 @@ module LeanWeb
|
|
23
24
|
@route = route
|
24
25
|
@request = request
|
25
26
|
@response = Rack::Response.new(nil, 200)
|
27
|
+
@content_for = {}
|
26
28
|
end
|
27
29
|
|
28
|
-
# Render magic. Supports
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# Depending on {Route#path} and {render} `path` you will render different
|
32
|
-
# documents:
|
33
|
-
#
|
34
|
-
# - {Route#path} `/a/b/c` and `render('d')` will render `src/views/a/b/d`
|
35
|
-
# - {Route#path} `/a/b/c/` and `render('d')` will render `src/views/a/b/c/d`
|
36
|
-
# - `render('~/custom')` will render `src/views/custom`.
|
37
|
-
# - `render('/custom')` will render `/custom`.
|
30
|
+
# Render magic. Supports every file extension that Tilt supports. Defaults
|
31
|
+
# to ERB when file extension is unknown.
|
38
32
|
#
|
39
33
|
# @param path [String] Might be an absolute path or a relative path to
|
40
|
-
# `src/views
|
41
|
-
# `@route.path`. You can also make it relative to {LeanWeb::VIEW_PATH} by
|
42
|
-
# prepending `~`.
|
34
|
+
# `src/views/`.
|
43
35
|
# @param content_type [String] Defaults to the proper Content-Type for file
|
44
36
|
# extension, `text/plain` on unknown files.
|
37
|
+
# @yield Optionally pass a block for nested rendering.
|
38
|
+
# @return [Rack::Request#finish] A valid Rack response.
|
39
|
+
def render_response(path, content_type = nil, &block)
|
40
|
+
template = create_template(path)
|
41
|
+
@response.set_header(
|
42
|
+
'Content-Type',
|
43
|
+
content_type || template.class.metadata[:mime_type] || 'text/plain'
|
44
|
+
)
|
45
|
+
@response.write(template.render(self){ block.call if block_given? })
|
46
|
+
@response.finish
|
47
|
+
end
|
48
|
+
|
49
|
+
# Template rendering engine. Useful for partials / nested views.
|
45
50
|
#
|
46
|
-
# @
|
47
|
-
|
51
|
+
# @param path [String] Same as on {#render_response}.
|
52
|
+
# @param options [Hash] Options for Tilt, defaults to
|
53
|
+
# template_defaults[extension].
|
54
|
+
# @return [String] Rendered `path`.
|
55
|
+
def create_template(path, options = nil)
|
48
56
|
path = absolute_view_path(path)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
57
|
+
ext = File.extname(path)[1..] || ''
|
58
|
+
ext = 'erb' unless Tilt.registered?(ext)
|
59
|
+
Tilt[ext].new(path, 1, options || template_defaults[ext] || {})
|
60
|
+
end
|
61
|
+
|
62
|
+
# Relative route to path from public directory considering current route.
|
63
|
+
#
|
64
|
+
# @param path [String] path from public directory, never begins with `/`.
|
65
|
+
def base_url(path = '.')
|
66
|
+
@base_url ||= @route.str_path[1..]
|
67
|
+
.sub(%r{[^/]*$}, '')
|
68
|
+
.gsub(%r{[^/]+}, '..')
|
69
|
+
|
70
|
+
@base_url + path
|
71
|
+
end
|
72
|
+
|
73
|
+
# Request params.
|
74
|
+
def params
|
75
|
+
@request.params
|
61
76
|
end
|
62
77
|
|
63
78
|
protected
|
64
79
|
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
case relative_path[0]
|
69
|
-
when '/'
|
70
|
-
relative_path
|
71
|
-
when '~'
|
72
|
-
relative_path.sub('~', LeanWeb::VIEW_PATH)
|
73
|
-
else
|
74
|
-
"#{view_path_by_route_path}/#{relative_path}"
|
75
|
-
end
|
80
|
+
# Set default options for Tilt in the format 'extension' => { options }.
|
81
|
+
def template_defaults
|
82
|
+
{}
|
76
83
|
end
|
77
84
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
elsif @route.path[-1] != '/'
|
83
|
-
File.dirname(@route.path)
|
84
|
-
else
|
85
|
-
@route.path.chomp('/')
|
86
|
-
end
|
85
|
+
# @param path [String]
|
86
|
+
# @return [String] Full path.
|
87
|
+
def absolute_view_path(path)
|
88
|
+
path[0] == '/' ? path : "#{LeanWeb::VIEW_PATH}/#{path}"
|
87
89
|
end
|
88
90
|
end
|
89
91
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
|
11
|
+
module LeanWeb
|
12
|
+
# String refinements.
|
13
|
+
module StringRefinements
|
14
|
+
refine ::String do
|
15
|
+
# String to PascalCase.
|
16
|
+
def pascalize
|
17
|
+
camelize(pascal: true)
|
18
|
+
end
|
19
|
+
|
20
|
+
# String to camelCase.
|
21
|
+
# @param pascal [Boolean] If true first letter is uppercase.
|
22
|
+
def camelize(pascal: false)
|
23
|
+
str = gsub(/[-_\s]+(.?)/){ |match| match[1].upcase }
|
24
|
+
str[0] = pascal ? str[0].upcase : str[0].downcase
|
25
|
+
str
|
26
|
+
end
|
27
|
+
|
28
|
+
# String to snake_case
|
29
|
+
def snakeize
|
30
|
+
gsub(/[[:upper:]]/){ |match| "_#{match.downcase}" }.delete_prefix('_')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/leanweb/route.rb
CHANGED
@@ -2,47 +2,60 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
|
+
require 'rack/mock'
|
10
11
|
|
11
12
|
module LeanWeb
|
12
13
|
# Action for {Route#action}.
|
13
|
-
Action = Struct.new(:
|
14
|
+
Action = Struct.new(:controller, :action, keyword_init: true)
|
14
15
|
|
15
|
-
# A single route which routes with the {respond} method. It can also {build}
|
16
|
+
# A single route which routes with the {#respond} method. It can also {#build}
|
16
17
|
# an static file.
|
17
18
|
class Route
|
19
|
+
using StringRefinements
|
20
|
+
|
18
21
|
attr_reader :path, :method, :action, :static
|
19
22
|
|
20
23
|
# A new instance of Route.
|
21
24
|
#
|
22
25
|
# @param path [String, Regexp] Path matcher, can be an String or Regexp with
|
23
26
|
# positional or named capture groups, `@action` will receive these as
|
24
|
-
# positional or named arguments.
|
27
|
+
# positional or named arguments. Always begins with `/`.
|
25
28
|
# @param method [String, nil] Must be an HTTP verb such as `GET` or `POST`.
|
26
29
|
# @param action [Proc, Hash, String, nil] References a Method/Proc to be
|
27
30
|
# triggered. It can be:
|
28
31
|
#
|
29
|
-
# -
|
30
|
-
#
|
31
|
-
#
|
32
|
-
# - A
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
32
|
+
# - Nothing, defaults to :"{#path_basename}_{#method}" (such as
|
33
|
+
# `:index_get`) on `:MainController`.
|
34
|
+
# - A full hash `{ controller: 'val', action: 'val' }`.
|
35
|
+
# - A hash with `{ Controller: :action }` only.
|
36
|
+
# - A simple string for an action on `:MainController`.
|
37
|
+
# - It can also be a `Proc`. Will be executed in a `:MainController`
|
38
|
+
# instance context.
|
36
39
|
#
|
37
|
-
# @param static [Boolean|Array] Defines a route as static.
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# params.
|
41
|
-
def initialize(path:, method: 'GET', action: nil, static:
|
40
|
+
# @param static [Boolean|Array] Defines a route as static. Defaults true for
|
41
|
+
# GET method, false otherwise. Set to `false` to say it can only work
|
42
|
+
# dynamically. You can also supply an array of arrays or hashes to
|
43
|
+
# generate static files based on that positional or keyword params.
|
44
|
+
def initialize(path:, method: 'GET', action: nil, static: nil)
|
42
45
|
@path = path
|
43
46
|
@method = method
|
44
47
|
self.action = action
|
45
|
-
@static = static
|
48
|
+
@static = static.nil? ? @method == 'GET' : static
|
49
|
+
end
|
50
|
+
|
51
|
+
# Check if route is available.
|
52
|
+
#
|
53
|
+
# If on production environment (not `development`), should serve only
|
54
|
+
# dynamic (not `static`) routes.
|
55
|
+
def available?
|
56
|
+
return @static == false if ENV['RACK_ENV'] != 'development'
|
57
|
+
|
58
|
+
true
|
46
59
|
end
|
47
60
|
|
48
61
|
# Last identifier on a path, returns `index` for `/`.
|
@@ -51,14 +64,14 @@ module LeanWeb
|
|
51
64
|
end
|
52
65
|
|
53
66
|
# @param request [Rack::Request]
|
54
|
-
# @return a valid rack response.
|
67
|
+
# @return [Array] a valid rack response.
|
55
68
|
def respond(request)
|
56
69
|
return respond_proc(request) if @action.instance_of?(Proc)
|
57
70
|
|
58
71
|
respond_method(request)
|
59
72
|
end
|
60
73
|
|
61
|
-
# String path, independent if {path} is Regexp or String.
|
74
|
+
# String path, independent if {#path} is Regexp or String.
|
62
75
|
def str_path
|
63
76
|
@path.source.gsub(/[\^$]/, '')
|
64
77
|
rescue NoMethodError
|
@@ -66,26 +79,26 @@ module LeanWeb
|
|
66
79
|
end
|
67
80
|
|
68
81
|
# On Regexp paths, return a string valid for making a request to this route.
|
82
|
+
#
|
69
83
|
# @param seed [Array, Hash] Seeds to use as replacement on capture groups.
|
70
84
|
# @return [String] sown path.
|
71
85
|
def seed_path(seed)
|
72
86
|
sown_path = str_path
|
73
87
|
if seed.instance_of?(Hash)
|
74
|
-
seed.each
|
88
|
+
seed.each{ |key, val| sown_path.sub!(/\(\?<#{key}>[^)]+\)/, val) }
|
75
89
|
else
|
76
|
-
seed.each
|
90
|
+
seed.each{ |val| sown_path.sub!(/\([^)]+\)/, val) }
|
77
91
|
end
|
78
92
|
sown_path
|
79
93
|
end
|
80
94
|
|
81
95
|
# Build this route as an static file and place it relative to
|
82
96
|
# {LeanWeb::PUBLIC_PATH}.
|
97
|
+
#
|
83
98
|
# @param request_path [String] Request path for dynamic (regex) routes.
|
84
99
|
def build(request_path = @path)
|
85
100
|
response = respond(
|
86
|
-
Rack::Request.new(
|
87
|
-
{ 'PATH_INFO' => request_path, 'REQUEST_METHOD' => 'GET' }
|
88
|
-
)
|
101
|
+
Rack::Request.new(Rack::MockRequest.env_for(request_path))
|
89
102
|
)
|
90
103
|
out_path = output_path(request_path, response[1]['Content-Type'] || nil)
|
91
104
|
FileUtils.mkdir_p(File.dirname(out_path))
|
@@ -97,48 +110,62 @@ module LeanWeb
|
|
97
110
|
|
98
111
|
# Assign value to `@action`.
|
99
112
|
def action=(value)
|
100
|
-
@action =
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
113
|
+
@action =
|
114
|
+
if value.instance_of?(Proc)
|
115
|
+
value
|
116
|
+
else
|
117
|
+
Action.new(**prepare_action_hash(value))
|
118
|
+
end
|
105
119
|
end
|
106
120
|
|
107
|
-
# @param
|
121
|
+
# @param src_value [Hash, String, nil] Check {#initialize} action param for
|
108
122
|
# valid input.
|
109
123
|
# @return [Hash] valid hash for {Action}.
|
110
|
-
def prepare_action_hash(
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
rescue NoMethodError
|
115
|
-
value = { action: value }
|
116
|
-
end
|
117
|
-
value[:file] ||= 'main'
|
118
|
-
value[:controller] ||= "#{value[:file].capitalize}Controller"
|
119
|
-
value[:action] ||= "#{path_basename}_#{@method.downcase}"
|
124
|
+
def prepare_action_hash(src_value)
|
125
|
+
value = prepare_prepare_action_hash(src_value)
|
126
|
+
value[:controller] = value[:controller]&.to_sym || DEFAULT_CONTROLLER
|
127
|
+
value[:action] = value[:action]&.to_sym || default_action_action
|
120
128
|
value
|
121
129
|
end
|
122
130
|
|
131
|
+
def prepare_prepare_action_hash(src_value)
|
132
|
+
if %i[controller action].include?(src_value.keys.first)
|
133
|
+
src_value
|
134
|
+
else
|
135
|
+
value = {}
|
136
|
+
value[:controller], value[:action] = src_value.first
|
137
|
+
value
|
138
|
+
end
|
139
|
+
rescue NoMethodError
|
140
|
+
{ action: src_value }
|
141
|
+
end
|
142
|
+
|
143
|
+
def default_action_action
|
144
|
+
"#{path_basename.gsub('-', '_')}_#{@method.downcase}".to_sym
|
145
|
+
end
|
146
|
+
|
123
147
|
# @param request [Rack::Request]
|
124
|
-
# @return a valid
|
148
|
+
# @return [Array] a valid Rack response.
|
125
149
|
def respond_method(request)
|
126
150
|
params = action_params(request.path)
|
127
|
-
require_relative("#{
|
151
|
+
require_relative("#{CONTROLLER_PATH}/#{@action.controller.to_s.snakeize}")
|
128
152
|
controller = Object.const_get(@action.controller).new(self, request)
|
129
|
-
return controller.public_send(@action.action, **params)\
|
153
|
+
return controller.public_send(@action.action, **params) \
|
130
154
|
if params.instance_of?(Hash)
|
131
155
|
|
132
156
|
controller.public_send(@action.action, *params)
|
133
157
|
end
|
134
158
|
|
135
159
|
# @param request [Rack::Request]
|
136
|
-
# @return a valid
|
160
|
+
# @return [Array] a valid Rack response.
|
137
161
|
def respond_proc(request)
|
138
162
|
params = action_params(request.path)
|
139
|
-
|
163
|
+
require_relative("#{CONTROLLER_PATH}/#{DEFAULT_CONTROLLER.to_s.snakeize}")
|
164
|
+
controller = Object.const_get(DEFAULT_CONTROLLER).new(self, request)
|
165
|
+
return controller.instance_exec(**params, &@action) \
|
166
|
+
if params.instance_of?(Hash)
|
140
167
|
|
141
|
-
|
168
|
+
controller.instance_exec(*params, &@action)
|
142
169
|
end
|
143
170
|
|
144
171
|
# @param request_path [String]
|
@@ -147,19 +174,30 @@ module LeanWeb
|
|
147
174
|
return nil unless @path.instance_of?(Regexp)
|
148
175
|
|
149
176
|
matches = @path.match(request_path)
|
150
|
-
return matches.named_captures.transform_keys(&:to_sym)\
|
177
|
+
return matches.named_captures.transform_keys(&:to_sym) \
|
151
178
|
if matches.named_captures != {}
|
152
179
|
|
153
180
|
matches.captures
|
154
181
|
end
|
155
182
|
|
156
183
|
# Output path for public file.
|
184
|
+
#
|
157
185
|
# @param path [String]
|
158
186
|
# @param content_type [String]
|
159
187
|
# @return [String] absolute route to path + extension based on content_type.
|
160
188
|
def output_path(path, content_type)
|
161
|
-
|
162
|
-
|
189
|
+
out_path =
|
190
|
+
if path[-1] == '/'
|
191
|
+
String.new("#{PUBLIC_PATH}#{path}/index")
|
192
|
+
else
|
193
|
+
String.new("#{PUBLIC_PATH}#{path}")
|
194
|
+
end
|
195
|
+
|
196
|
+
unless path.end_with?(MEDIA_EXTENSIONS[content_type])
|
197
|
+
out_path << MEDIA_EXTENSIONS[content_type]
|
198
|
+
end
|
199
|
+
|
200
|
+
out_path
|
163
201
|
end
|
164
202
|
end
|
165
203
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
|
11
|
+
module LeanWeb
|
12
|
+
VERSION = '0.3.0'
|
13
|
+
end
|
data/lib/leanweb.rb
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
10
|
# LeanWeb is a minimal hybrid static / dynamic web framework.
|
11
11
|
module LeanWeb
|
12
|
-
|
12
|
+
require_relative 'leanweb/version'
|
13
13
|
|
14
|
-
ROOT_PATH = ENV
|
14
|
+
ROOT_PATH = ENV.fetch('LEANWEB_ROOT_PATH', Dir.pwd)
|
15
15
|
CONTROLLER_PATH = "#{ROOT_PATH}/src/controllers"
|
16
16
|
VIEW_PATH = "#{ROOT_PATH}/src/views"
|
17
17
|
PUBLIC_PATH = "#{ROOT_PATH}/public"
|
@@ -21,10 +21,16 @@ module LeanWeb
|
|
21
21
|
'application/javascript' => '.js',
|
22
22
|
'application/json' => '.json',
|
23
23
|
'text/html' => '.html',
|
24
|
-
'text/plain' => '.txt'
|
24
|
+
'text/plain' => '.txt',
|
25
|
+
'text/css' => '.css',
|
26
|
+
'text/csv' => '.csv'
|
25
27
|
}.freeze
|
26
28
|
|
29
|
+
DEFAULT_CONTROLLER = :MainController
|
30
|
+
|
27
31
|
autoload :Route, 'leanweb/route.rb'
|
28
32
|
autoload :Controller, 'leanweb/controller.rb'
|
29
33
|
autoload :App, 'leanweb/app.rb'
|
30
34
|
end
|
35
|
+
|
36
|
+
require_relative 'leanweb/helpers'
|