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.
@@ -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 Hacktivista
6
- # General Public License version 0.1 or (at your option) any later version. You
7
- # should have received a copy of this license along with the software. If not,
8
- # see <https://hacktivista.org/licenses/>.
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 {render}
15
- # Haml documents.
15
+ # private attributes that will be shared with your views when you
16
+ # {#render_response}.
16
17
  #
17
- # Even if you don't {render}, you can use the `.finish` method from
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 dynamic (and static) Haml documents, other
29
- # documents don't support dynamic data.
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/` + the parent path of the last non-capture-group on
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
- # @return [Rack::Request#finish] A valid rack response.
47
- def render(path, content_type = nil) # rubocop:disable Metrics/MethodLength
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
- content = File.read(path)
50
- case File.extname(path)
51
- when '.haml'
52
- require('haml')
53
- @response.write(Haml::Engine.new(content).render(binding))
54
- @response.set_header('Content-Type', content_type || 'text/html')
55
- else
56
- require('erb')
57
- @response.write(ERB.new(content).result(binding))
58
- @response.set_header('Content-Type', content_type || 'text/plain')
59
- end
60
- @response.finish
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
- # @param relative_path [String]
66
- # @return [String] Full path.
67
- def absolute_view_path(relative_path)
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
- def view_path_by_route_path
79
- LeanWeb::VIEW_PATH +
80
- if @route.path.instance_of?(Regexp)
81
- File.dirname(@route.str_path.sub(%r{/?\(.*}, ''))
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 Hacktivista
6
- # General Public License version 0.1 or (at your option) any later version. You
7
- # should have received a copy of this license along with the software. If not,
8
- # see <https://hacktivista.org/licenses/>.
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(:file, :controller, :action, keyword_init: true)
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
- # - A full hash `{ file: 'val', controller: 'val', action: 'val' }`.
30
- # - A hash with `{ 'file' => 'action' }` only (which has a controller
31
- # class name `{File}Controller`).
32
- # - A simple string (which will consider file `main.rb` and controller
33
- # `MainController`). Defaults to "{path_basename}_{method}", (ex:
34
- # `index_get`).
35
- # - It can also be a `Proc`.
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. Set to `false` to
38
- # say it can only work dynamically. You can also supply an array of arrays
39
- # or hashes to generate static files based on that positional or keyword
40
- # params.
41
- def initialize(path:, method: 'GET', action: nil, static: true)
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 { |key, val| sown_path.sub!(/\(\?<#{key}>[^)]+\)/, val) }
88
+ seed.each{ |key, val| sown_path.sub!(/\(\?<#{key}>[^)]+\)/, val) }
75
89
  else
76
- seed.each { |val| sown_path.sub!(/\([^)]+\)/, val) }
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 = if value.instance_of?(Proc)
101
- value
102
- else
103
- Action.new(**prepare_action_hash(value))
104
- end
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 value [Hash, String, nil] Check {initialize} action param for
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(value)
111
- begin
112
- value[:file], value[:action] = value.first \
113
- unless %i[file controller action].include?(value.keys.first)
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 rack response.
148
+ # @return [Array] a valid Rack response.
125
149
  def respond_method(request)
126
150
  params = action_params(request.path)
127
- require_relative("#{LeanWeb::CONTROLLER_PATH}/#{@action.file}")
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 rack response.
160
+ # @return [Array] a valid Rack response.
137
161
  def respond_proc(request)
138
162
  params = action_params(request.path)
139
- return @action.call(**params) if params.instance_of?(Hash)
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
- @action.call(*params)
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
- path += 'index' if path[-1] == '/'
162
- "#{LeanWeb::PUBLIC_PATH}#{path}#{LeanWeb::MEDIA_EXTENSIONS[content_type]}"
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 Hacktivista
6
- # General Public License version 0.1 or (at your option) any later version. You
7
- # should have received a copy of this license along with the software. If not,
8
- # see <https://hacktivista.org/licenses/>.
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
- VERSION = '0.1.3'
12
+ require_relative 'leanweb/version'
13
13
 
14
- ROOT_PATH = ENV['LEANWEB_ROOT_PATH'] || Dir.pwd
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'