leanweb 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da54199f1863128e9bb23baf728dff6be60123b9cf44947370293222816da122
4
- data.tar.gz: 60e11d76704347858cdffbde24d17b5d0edf1061ea91ef158dd5a6928e48bc4c
3
+ metadata.gz: 9c2c8b263119a2bfb7a4a812f4ffeb66bc9444bfc65a5ea0ab99d627d227e307
4
+ data.tar.gz: bbb016308a12268ee78694e378e931c016475ca8cf9ff4264d017f79664911f3
5
5
  SHA512:
6
- metadata.gz: a3b3abccd00de5d76a06513c021b409d19073c911f3d236b7c7412c42e723bad9e616514a02e278e55271d5faf5709f94fb283af6fa93aa873e2144ff0d3211b
7
- data.tar.gz: a366dce05e7a8108e7b3ca4096112a3567e8082a0934e64d323401a938dceb24bfdc6c03dd53193827800b04a05c1c4c9ab53189cc45453e7925422dfb58535b
6
+ metadata.gz: 52c6e7bdd02a5c2797d20cd6979574cec8a4d8333ab659f3332fa6b4e3bce906a69f950124453621e12dee38c0c585ac3abace0a8041c54d444524c0a21bbba3
7
+ data.tar.gz: 64ec617d144264edce4922fa2258173f6cc139a20102a9ba4c4290adb6ec06d13be4b5f265af715fdcd82005ae03c66ac447c7dd79bc30dce0c3235d4a4f8e22
data/bin/leanweb CHANGED
@@ -3,10 +3,10 @@
3
3
 
4
4
  # Copyright 2022 Felix Freeman <libsys@hacktivista.org>
5
5
  #
6
- # This file is part of "LeanWeb" and licensed under the terms of the Hacktivista
7
- # General Public License version 0.1 or (at your option) any later version. You
8
- # should have received a copy of this license along with the software. If not,
9
- # see <https://hacktivista.org/licenses/>.
6
+ # This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
7
+ # General Public License version 3 with permissions for compatibility with the
8
+ # Hacktivista General Public License. You should have received a copy of this
9
+ # license along with the software. If not, see <https://gnu.org/licenses/>.
10
10
 
11
11
  require 'fileutils'
12
12
  require 'leanweb'
@@ -16,15 +16,15 @@ require 'leanweb'
16
16
  files = [
17
17
  {
18
18
  filename: 'Gemfile',
19
- # TODO: Use {LeanWeb::VERSION} when v1 is reached.
20
19
  content: <<~RUBY
21
20
  # frozen_string_literal: true
22
21
 
23
22
  source 'https://rubygems.org'
24
23
 
25
- gem 'leanweb', '~> 0.2'
26
- gem 'haml'
24
+ gem 'leanweb', '~> #{LeanWeb::VERSION}'
25
+ gem 'haml', '~> 5.2.2'
27
26
  gem 'rake'
27
+ gem 'puma'
28
28
  RUBY
29
29
  }, {
30
30
  filename: 'config.ru',
@@ -55,7 +55,7 @@ files = [
55
55
 
56
56
  require 'leanweb'
57
57
 
58
- task default: %w[build]
58
+ task default: %w[build_static]
59
59
 
60
60
  task :build_static do
61
61
  require_relative 'routes'
@@ -63,7 +63,7 @@ files = [
63
63
  end
64
64
  RUBY
65
65
  }, {
66
- filename: 'src/controllers/main.rb',
66
+ filename: 'src/controllers/main_controller.rb',
67
67
  content: <<~RUBY
68
68
  # frozen_string_literal: true
69
69
 
@@ -88,6 +88,12 @@ files = [
88
88
  %body
89
89
  %h1 It works!
90
90
  HAML
91
+ }, {
92
+ filename: '.gitignore'
93
+ content: <<~GITIGNORE
94
+ /.bundle
95
+ /public/**/*.html
96
+ GITIGNORE
91
97
  }
92
98
  ]
93
99
 
@@ -0,0 +1,76 @@
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
+ require 'json'
11
+ require 'net/http'
12
+ require 'openssl'
13
+ require 'tilt'
14
+ require 'uri'
15
+
16
+ # Hawese client.
17
+ #
18
+ # **Environment variables**
19
+ #
20
+ # - HAWESE_RETURN_URL defaults to `LEANWEB_ENDPOINT/checkout`.
21
+ # - HAWESE_ENDPOINT
22
+ # - HAWESE_ORIGIN
23
+ class Hawese
24
+ class << self
25
+ ENDPOINT = ENV.fetch('HAWESE_ENDPOINT')
26
+ RETURN_URL = ENV.fetch('HAWESE_RETURN_URL') do
27
+ "#{ENV.fetch('LEANWEB_ENDPOINT')}/checkout"
28
+ end
29
+ ORIGIN = ENV.fetch('HAWESE_ORIGIN')
30
+
31
+ def payment_methods(country = '*', currency = nil)
32
+ endpoint = String.new(
33
+ "#{ENDPOINT}/gateways/payment-methods/purchase?country=#{country}"
34
+ )
35
+ endpoint << "&currency=#{currency}" if currency
36
+ uri = URI(endpoint)
37
+ JSON.parse(Net::HTTP.get(uri), symbolize_names: true)
38
+ end
39
+
40
+ def gateway_schema(gateway, query_params, schema = 'purchase')
41
+ uri = URI("#{ENDPOINT}/gateways/#{gateway}/schemas/#{schema}")
42
+ uri.query = URI.encode_www_form(query_params)
43
+ JSON.parse(Net::HTTP.get(uri), symbolize_names: true)
44
+ end
45
+
46
+ def purchase(gateway, fields)
47
+ fields[:origin] = ORIGIN
48
+ uri = URI("#{ENDPOINT}/gateways/#{gateway}/purchase")
49
+ uri.query = URI.encode_www_form(return_url: RETURN_URL)
50
+ response = Net::HTTP.post(
51
+ uri,
52
+ fields.to_json,
53
+ 'Content-Type' => 'application/json'
54
+ )
55
+ JSON.parse(response.body, symbolize_names: true)
56
+ end
57
+
58
+ def purchase_from_schema(gateway, schema)
59
+ fields = {}
60
+ schema[:properties].each do |property, contents|
61
+ fields[property] = contents[:default] if contents.include?(:default)
62
+ end
63
+ purchase(gateway, fields)
64
+ end
65
+
66
+ def payment(uuid)
67
+ uri = URI("#{ENDPOINT}/payments/#{uuid}")
68
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
69
+ req = Net::HTTP::Get.new(uri)
70
+ req['Authorization'] = "Bearer #{ENV.fetch('HAWESE_AUTH_TOKEN')}"
71
+ response = http.request(req)
72
+ JSON.parse(response.body, symbolize_names: true)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,125 @@
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
+ require 'net/smtp'
11
+ require 'securerandom'
12
+ require 'socket'
13
+ require 'time'
14
+
15
+ # Send an email with SMTP easily.
16
+ #
17
+ # Environment variables:
18
+ #
19
+ # - SMTP_HOST: Where to connect to.
20
+ # - SMTP_PORT: Port, optional (default: 25).
21
+ # - SMTP_SECURITY: `tls` or `starttls`, optional (default: `nil`).
22
+ # - SMTP_USER: User, optional.
23
+ # - SMTP_PASSWORD: Password, optional.
24
+ # - SMTP_FROM: In the format `Name <user@mail>` or `user@mail`.
25
+ #
26
+ # @example Single address
27
+ # LeanMail.deliver('to@mail', 'subject', 'body')
28
+ #
29
+ # @example Multiple addresses
30
+ # LeanMail.deliver(['to@mail', 'to2@mail'], 'subject', 'body')
31
+ module LeanMail
32
+ # RFC 2821 message data representation.
33
+ class Data
34
+ attr_reader :from, :to, :subject, :message
35
+
36
+ def initialize(from, to, subject, message)
37
+ @from = from
38
+ @to = to
39
+ @subject = subject
40
+ @message = message
41
+ end
42
+
43
+ def to_s
44
+ <<~MAIL
45
+ From: #{@from}
46
+ To: #{to.instance_of?(Array) ? to.join(', ') : to}
47
+ Subject: #{subject}
48
+ Date: #{Time.new.rfc2822}
49
+ Message-Id: <#{SecureRandom.uuid}@#{Socket.gethostname}>
50
+
51
+ #{@message}
52
+ MAIL
53
+ end
54
+ end
55
+
56
+ # Email deliverer.
57
+ class Deliver
58
+ attr_reader :data
59
+
60
+ def initialize
61
+ @user = ENV.fetch('SMTP_USER', nil)
62
+ @password = ENV.fetch('SMTP_PASSWORD', nil)
63
+ @from = ENV.fetch('SMTP_FROM')
64
+
65
+ @host = ENV.fetch('SMTP_HOST')
66
+ @port = ENV.fetch('SMTP_PORT', 25)
67
+ @security = ENV.fetch('SMTP_SECURITY', nil)
68
+
69
+ initialize_smtp
70
+ end
71
+
72
+ # Send email.
73
+ #
74
+ # @param to [Array, String] In the format `Name <user@mail>` or `user@mail`.
75
+ # @param subject [String]
76
+ # @param body [String]
77
+ # @return [Deliver]
78
+ def call(to, subject, body)
79
+ @data = Data.new(@from, to, subject, body)
80
+
81
+ @smtp.start(Socket.gethostname, @user, @password, :plain) do |smtp|
82
+ smtp.send_message(@data.to_s, extract_addr(@from), extract_addrs(to))
83
+ end
84
+
85
+ self
86
+ end
87
+
88
+ protected
89
+
90
+ def initialize_smtp
91
+ @smtp = Net::SMTP.new(@host, @port)
92
+ maybe_enable_security(@security)
93
+ end
94
+
95
+ def maybe_enable_security(security)
96
+ case security
97
+ when 'tls' then @smtp.enable_tls
98
+ when 'starttls' then @smtp.enable_starttls
99
+ end
100
+ end
101
+
102
+ def extract_addr(str)
103
+ match = str.match(%r{<([^/]+)>})
104
+ return match[1] if match
105
+
106
+ str
107
+ end
108
+
109
+ def extract_addrs(to)
110
+ return to.map{ |addr| extract_addr(addr) } if to.instance_of?(Array)
111
+
112
+ extract_addr(to)
113
+ end
114
+ end
115
+
116
+ # Deliver email.
117
+ #
118
+ # @param to [Array, String] In the format `Name <user@mail>` or `user@mail`.
119
+ # @param subject [String]
120
+ # @param body [String]
121
+ # @return [Deliver]
122
+ def self.deliver(to, subject, body)
123
+ LeanMail::Deliver.new.call(to, subject, body)
124
+ end
125
+ end
@@ -0,0 +1,205 @@
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
+ module ControllerMixins
13
+ # {render_with_layout} for {Controller} with ERB, HAML, Tilt::EmacsOrg and
14
+ # Redcarpet Markdown support.
15
+ module RenderWithLayout # rubocop:disable Metrics/ModuleLength
16
+ # Render a response with a layout.
17
+ #
18
+ # Depending on the `path` file contents and options `@content_for` might
19
+ # be filled. These contents must be inserted on the `head` or `body` of
20
+ # your layout file.
21
+ #
22
+ # All paths used on parameters can be relative to {VIEW_PATH} or absolute.
23
+ #
24
+ # @param path [String] Path to file to rendered.
25
+ # @param title [String] `@content_for[:title]` content.
26
+ # @param head [Hash] Things to be inserted on `@content_for[:head]`.
27
+ # @option head [String|Array] :js Javascript file path(s).
28
+ # @option head [String|Array] :jsm Javascript module file path (s).
29
+ # @option head [String|Array] :css CSS file path(s).
30
+ # @option head [String|Array] :raw Raw headers in HTML format.
31
+ # @param layout [String] Layout file path.
32
+ # @param sub_layout [String] Layout file to be inserted within layout.
33
+ # @param options [Hash] Options hash, to be given to the template engine
34
+ # based on file extension: `Tilt::EmacsOrg` for `.org` and `Redcarpet`
35
+ # for `.md`.
36
+ # @option options [String] :setupfile For `.org`.
37
+ # @option options [true] :toc For `.md`. To fill `@content_for[:toc]`
38
+ # render with option `:with_toc_data`.
39
+ # @yield On dynamic templates (such as Haml and ERB) you can yield.
40
+ def render_with_layout( # rubocop:disable Metrics/ParameterLists
41
+ path,
42
+ title: nil,
43
+ head: nil,
44
+ layout: 'layout.haml',
45
+ sub_layout: nil,
46
+ options: nil,
47
+ &block
48
+ )
49
+ @content_for[:title] = title unless title.nil?
50
+ content = render_by_extension(path, sub_layout, options, &block)
51
+ prepare_head(**head) unless head.nil?
52
+ render_response(layout, 'text/html'){ content }
53
+ end
54
+
55
+ # Render response for missing static action methods. Called from
56
+ # {Route#respond}.
57
+ def default_static_action(view_path)
58
+ render_with_layout(view_path)
59
+ end
60
+
61
+ protected
62
+
63
+ # @param path [String]
64
+ # @param layout [String]
65
+ # @param options [Hash] Check `Tilt::EmacsOrg` options.
66
+ # @option options [String] :setupfile can be absolute or relative to
67
+ # {VIEW_PATH}.
68
+ def render_org(path, layout: nil, options: {})
69
+ options[:setupfile] = absolute_view_path(options[:setupfile]) \
70
+ if options.include?(:setupfile)
71
+
72
+ org_template = create_template(path, options)
73
+
74
+ @content_for[:title] = org_template.title \
75
+ unless @content_for.include?(:title)
76
+
77
+ if layout
78
+ create_template(layout).render(self){ org_template.render(self) }
79
+ else
80
+ org_template.render(self)
81
+ end
82
+ end
83
+
84
+ # @param path [String]
85
+ # @param layout [String]
86
+ # @param options [Hash] Check `RedCarpet` `render_options`.
87
+ # @option options [true] :toc To place an HTML based table of contents on
88
+ # `@content_for[:toc]` and render with option `:with_toc_data`.
89
+ def render_markdown(path, layout: nil, options: {})
90
+ maybe_render_markdown_toc!(path, options)
91
+ markdown_template = create_template(path, options)
92
+
93
+ @content_for[:title] = find_title('md', path) \
94
+ unless @content_for.include?(:title)
95
+
96
+ if layout
97
+ create_template(layout).render(self){ markdown_template.render(self) }
98
+ else
99
+ markdown_template.render(self)
100
+ end
101
+ end
102
+
103
+ # If `options[:toc]` is set, deletes options `:toc` and adds
104
+ # `:with_toc_data`. Also adds `@content_for[:toc]`.
105
+ def maybe_render_markdown_toc!(path, options)
106
+ return unless options.include?(:toc)
107
+
108
+ require('redcarpet')
109
+ @content_for[:toc] = create_template(
110
+ path, { renderer: Redcarpet::Render::HTML_TOC }
111
+ ).render
112
+
113
+ options.delete(:toc)
114
+ options[:with_toc_data] = true
115
+ end
116
+
117
+ def find_title(ext, path)
118
+ regex = find_title_ext_regex(ext)
119
+ title = nil
120
+ File.foreach(absolute_view_path(path)) do |line|
121
+ matches = line.match(regex)
122
+ (title = matches[1]) && break if matches
123
+ end
124
+ title
125
+ end
126
+
127
+ def find_title_ext_regex(ext)
128
+ case ext
129
+ when 'md' then /#(?!#)\s?(.+)/
130
+ when 'haml' then /%h1[^\s]*\s(.+)/
131
+ end
132
+ end
133
+
134
+ # @param path [String]
135
+ # @param layout [String]
136
+ # @param options [Hash] Check `Haml` options.
137
+ # @yield If block given.
138
+ def render_other(path, layout: nil, options: {})
139
+ template = create_template(path, options)
140
+ if layout
141
+ create_template(layout).render(self) do
142
+ template.render(self){ yield if block_given? }
143
+ end
144
+ else
145
+ template.render(self){ yield if block_given? }
146
+ end
147
+ end
148
+
149
+ # rubocop:disable Metrics/MethodLength
150
+ def render_by_extension(path, layout, options, &block)
151
+ ext = File.extname(path)[1..]
152
+ options = options || template_defaults[ext] || {}
153
+
154
+ case ext
155
+ when 'org'
156
+ return render_org(path, layout: layout, options: options)
157
+ when 'md'
158
+ return render_markdown(path, layout: layout, options: options)
159
+ when 'haml'
160
+ @content_for[:title] = find_title('haml', path) \
161
+ unless @content_for.include?(:title)
162
+ end
163
+
164
+ render_other(path, layout: layout, options: options, &block)
165
+ end
166
+ # rubocop:enable Metrics/MethodLength
167
+
168
+ def prepare_head(js: nil, jsm: nil, css: nil, raw: nil)
169
+ head = String.new
170
+ head << prepare_head_js(js) unless js.nil?
171
+ head << prepare_head_js(jsm, 'module') unless jsm.nil?
172
+ head << prepare_head_css(css) unless css.nil?
173
+ head << prepare_head_raw(raw) unless raw.nil?
174
+ @content_for[:head] = head
175
+ end
176
+
177
+ def prepare_head_js(js, type = 'application/javascript')
178
+ head = String.new
179
+ js = [js] if js.instance_of?(String)
180
+ js.each do |src|
181
+ head << "<script type='#{type}' src='#{src}'"
182
+ head << ' defer' if type != 'module'
183
+ head << "></script>\n"
184
+ end
185
+ head
186
+ end
187
+
188
+ def prepare_head_css(css)
189
+ head = String.new
190
+ css = [css] if css.instance_of?(String)
191
+ css.each do |src|
192
+ head << "<link rel='stylesheet' type='text/css' href='#{src}'>\n"
193
+ end
194
+ head
195
+ end
196
+
197
+ def prepare_head_raw(raw)
198
+ head = String.new
199
+ raw = [raw] if raw.instance_of?(String)
200
+ raw.each{ |src| head << "#{src}\n" }
201
+ head
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,84 @@
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
+ require 'date'
11
+ require 'fileutils'
12
+ require 'json'
13
+ require 'rack/session/abstract/id'
14
+
15
+ module Rack
16
+ module Session
17
+ # A JSON File based session storage.
18
+ class JsonFile < Abstract::PersistedSecure
19
+ # Cleans session files that have not been used.
20
+ # @param to [Time|Date] Time from which file has not been accessed.
21
+ # Defaults to 00:00 hrs 1 month ago.
22
+ # @param dir [String] Directory from which to clean files. Defaults to
23
+ # `/tmp/rack.session`.
24
+ def self.clean(
25
+ to = Date.today.prev_month,
26
+ dir = "#{Dir.tmpdir}/rack.session"
27
+ )
28
+ to = to.to_time if to.instance_of?(Date)
29
+ Dir["#{dir}/*"].each do |file|
30
+ FileUtils.rm(file, force: true) if ::File.atime(file) < to
31
+ end
32
+ end
33
+
34
+ # @param options [Hash] Accepts a :session_dir path which defaults to
35
+ # `/tmp/rack.session`.
36
+ def initialize(app, options = {})
37
+ super(
38
+ app,
39
+ {
40
+ secure: ENV['RACK_ENV'] != 'development', # HTTPS unless in dev env
41
+ same_site: 'Strict' # don't allow cross-site sessions
42
+ }.merge!(options)
43
+ )
44
+ @session_dir = "#{Dir.tmpdir}/#{@key}" || @default_options[:session_dir]
45
+ end
46
+
47
+ # @return [Array] [self, Hash].
48
+ def find_session(_req, sid)
49
+ sid = generate_sid if sid.nil?
50
+
51
+ [sid, JSON.parse(::File.read("#{@session_dir}/#{sid}"))]
52
+ rescue Errno::ENOENT
53
+ [generate_sid, {}]
54
+ rescue JSON::ParserError
55
+ [sid, {}]
56
+ end
57
+
58
+ # @param sid [String] Only known ids are allowed, to avoid session
59
+ # fixation attacks.
60
+ # @return [self|false] session_id or false.
61
+ def write_session(_req, sid, session, _options)
62
+ return false unless ::File.exist?("#{@session_dir}/#{sid}")
63
+
64
+ ::File.open("#{@session_dir}/#{sid}", 'w', 0o600) do |f|
65
+ f << JSON.generate(session)
66
+ end
67
+ sid
68
+ end
69
+
70
+ # @return [self|nil] new session id or nil if options[:drop].
71
+ def delete_session(_req, sid, options)
72
+ FileUtils.rm("#{@session_dir}/#{sid}", force: true)
73
+ options&.include?(:drop) ? nil : generate_sid
74
+ end
75
+
76
+ def generate_sid(*)
77
+ sid = super
78
+ FileUtils.mkdir_p(@session_dir, mode: 0o700)
79
+ ::File.new("#{@session_dir}/#{sid}", 'w', 0o600).close
80
+ sid
81
+ end
82
+ end
83
+ end
84
+ end
data/lib/leanweb/app.rb CHANGED
@@ -2,10 +2,10 @@
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
11
 
@@ -2,10 +2,10 @@
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
11
  require 'tilt'
@@ -24,6 +24,7 @@ module LeanWeb
24
24
  @route = route
25
25
  @request = request
26
26
  @response = Rack::Response.new(nil, 200)
27
+ @content_for = {}
27
28
  end
28
29
 
29
30
  # Render magic. Supports every file extension that Tilt supports. Defaults
@@ -58,6 +59,12 @@ module LeanWeb
58
59
  Tilt[ext].new(path, 1, options || template_defaults[ext] || {})
59
60
  end
60
61
 
62
+ # Render response for missing static action methods. Called from
63
+ # {Route#respond}.
64
+ def default_static_action(view_path)
65
+ render_response(view_path)
66
+ end
67
+
61
68
  # Relative route to path from public directory considering current route.
62
69
  #
63
70
  # @param path [String] path from public directory, never begins with `/`.
@@ -69,9 +76,28 @@ module LeanWeb
69
76
  @base_url + path
70
77
  end
71
78
 
79
+ # Get absolute path for a file within {VIEW_PATH}.
80
+ # @param path [String] Can be:
81
+ # - A full path, starts with `/`.
82
+ # - A path relative to {VIEW_PATH}.
83
+ # - A path relative to current @route.path directory, starts with `./`.
84
+ # @return [String] Absolute path.
85
+ def absolute_view_path(path)
86
+ return path if path.start_with?('/')
87
+
88
+ view_path = String.new(LeanWeb::VIEW_PATH)
89
+
90
+ if path.start_with?('./')
91
+ view_path << @route.path.sub(%r{/[^/]*$}, '')
92
+ path = path[2..]
93
+ end
94
+
95
+ path == '' ? view_path : "#{view_path}/#{path}"
96
+ end
97
+
72
98
  # Request params.
73
99
  def params
74
- @request.params
100
+ @request&.params
75
101
  end
76
102
 
77
103
  protected
@@ -80,11 +106,5 @@ module LeanWeb
80
106
  def template_defaults
81
107
  {}
82
108
  end
83
-
84
- # @param path [String]
85
- # @return [String] Full path.
86
- def absolute_view_path(path)
87
- path[0] == '/' ? path : "#{LeanWeb::VIEW_PATH}/#{path}"
88
- end
89
109
  end
90
110
  end
@@ -2,23 +2,33 @@
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
- # String helpers.
11
- class String
12
- # String to PascalCase.
13
- def pascalize
14
- camelize(pascal: true)
15
- end
16
10
 
17
- # String to camelCase.
18
- # @param pascal [Boolean] If true first letter is uppercase.
19
- def camelize(pascal: false)
20
- str = gsub(/[-_\s]+(.?)/){ |match| match[1].upcase }
21
- str[0] = pascal ? str[0].upcase : str[0].downcase
22
- str
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
23
33
  end
24
34
  end
data/lib/leanweb/route.rb CHANGED
@@ -2,20 +2,22 @@
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/mock'
11
11
 
12
12
  module LeanWeb
13
13
  # Action for {Route#action}.
14
- Action = Struct.new(:file, :controller, :action, keyword_init: true)
14
+ Action = Struct.new(:controller, :action, keyword_init: true)
15
15
 
16
16
  # A single route which routes with the {#respond} method. It can also {#build}
17
17
  # an static file.
18
18
  class Route
19
+ using StringRefinements
20
+
19
21
  attr_reader :path, :method, :action, :static
20
22
 
21
23
  # A new instance of Route.
@@ -27,13 +29,13 @@ module LeanWeb
27
29
  # @param action [Proc, Hash, String, nil] References a Method/Proc to be
28
30
  # triggered. It can be:
29
31
  #
30
- # - A full hash `{ file: 'val', controller: 'val', action: 'val' }`.
31
- # - A hash with `{ 'file' => 'action' }` only (which has a controller
32
- # class name `{File}Controller`).
33
- # - A simple string (which will consider file `main.rb` and controller
34
- # `MainController`). Defaults to "{#path_basename}_{#method}", (ex:
35
- # `index_get`).
36
- # - 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.
37
39
  #
38
40
  # @param static [Boolean|Array] Defines a route as static. Defaults true for
39
41
  # GET method, false otherwise. Set to `false` to say it can only work
@@ -61,12 +63,20 @@ module LeanWeb
61
63
  str_path[-1] == '/' ? 'index' : File.basename(str_path)
62
64
  end
63
65
 
66
+ # Respond with a proc, controller method, or in case of true static routes
67
+ # a rendering of {VIEW_PATH}/{path} with any file extension.
68
+ #
64
69
  # @param request [Rack::Request]
65
70
  # @return [Array] a valid rack response.
66
71
  def respond(request)
67
72
  return respond_proc(request) if @action.instance_of?(Proc)
68
73
 
69
74
  respond_method(request)
75
+ rescue NoMethodError
76
+ raise unless @static == true && (view_path = guess_view_path)
77
+
78
+ controller = default_controller_class.new(self)
79
+ controller.default_static_action(view_path)
70
80
  end
71
81
 
72
82
  # String path, independent if {#path} is Regexp or String.
@@ -108,42 +118,52 @@ module LeanWeb
108
118
 
109
119
  # Assign value to `@action`.
110
120
  def action=(value)
111
- @action = if value.instance_of?(Proc)
112
- value
113
- else
114
- Action.new(**prepare_action_hash(value))
115
- end
121
+ @action =
122
+ if value.instance_of?(Proc)
123
+ value
124
+ else
125
+ Action.new(**prepare_action_hash(value))
126
+ end
116
127
  end
117
128
 
118
129
  # @param src_value [Hash, String, nil] Check {#initialize} action param for
119
130
  # valid input.
120
131
  # @return [Hash] valid hash for {Action}.
121
- def prepare_action_hash(src_value) # rubocop:disable Metrics/MethodLength
122
- begin
123
- if %i[file controller action].include?(src_value.keys.first)
124
- value = src_value
125
- else
126
- value = {}
127
- value[:file], value[:action] = src_value.first
128
- end
129
- rescue NoMethodError
130
- value = { action: src_value }
131
- end
132
- value[:file] ||= 'main'
133
- value[:controller] ||= "#{value[:file].pascalize}Controller"
134
- value[:action] ||= default_action_name
132
+ def prepare_action_hash(src_value)
133
+ value = prepare_prepare_action_hash(src_value)
134
+ value[:controller] = value[:controller]&.to_sym || DEFAULT_CONTROLLER
135
+ value[:action] = value[:action]&.to_sym || default_action_action
135
136
  value
136
137
  end
137
138
 
138
- def default_action_name
139
- "#{path_basename.gsub('-', '_')}_#{@method.downcase}"
139
+ def prepare_prepare_action_hash(src_value)
140
+ if %i[controller action].include?(src_value.keys.first)
141
+ src_value
142
+ else
143
+ value = {}
144
+ value[:controller], value[:action] = src_value.first
145
+ value
146
+ end
147
+ rescue NoMethodError
148
+ { action: src_value }
149
+ end
150
+
151
+ def default_action_action
152
+ "#{path_basename.gsub(/[.-]/, '_')}_#{@method.downcase}".to_sym
153
+ end
154
+
155
+ def default_controller_class
156
+ require_relative("#{CONTROLLER_PATH}/#{DEFAULT_CONTROLLER.to_s.snakeize}")
157
+ Object.const_get(DEFAULT_CONTROLLER)
158
+ rescue LoadError
159
+ Controller
140
160
  end
141
161
 
142
162
  # @param request [Rack::Request]
143
163
  # @return [Array] a valid Rack response.
144
164
  def respond_method(request)
145
165
  params = action_params(request.path)
146
- require_relative("#{LeanWeb::CONTROLLER_PATH}/#{@action.file}")
166
+ require_relative("#{CONTROLLER_PATH}/#{@action.controller.to_s.snakeize}")
147
167
  controller = Object.const_get(@action.controller).new(self, request)
148
168
  return controller.public_send(@action.action, **params) \
149
169
  if params.instance_of?(Hash)
@@ -155,9 +175,11 @@ module LeanWeb
155
175
  # @return [Array] a valid Rack response.
156
176
  def respond_proc(request)
157
177
  params = action_params(request.path)
158
- return @action.call(**params) if params.instance_of?(Hash)
178
+ controller = default_controller_class.new(self, request)
179
+ return controller.instance_exec(**params, &@action) \
180
+ if params.instance_of?(Hash)
159
181
 
160
- @action.call(*params)
182
+ controller.instance_exec(*params, &@action)
161
183
  end
162
184
 
163
185
  # @param request_path [String]
@@ -178,8 +200,28 @@ module LeanWeb
178
200
  # @param content_type [String]
179
201
  # @return [String] absolute route to path + extension based on content_type.
180
202
  def output_path(path, content_type)
181
- path += 'index' if path[-1] == '/'
182
- "#{LeanWeb::PUBLIC_PATH}#{path}#{LeanWeb::MEDIA_EXTENSIONS[content_type]}"
203
+ out_path =
204
+ if path[-1] == '/'
205
+ String.new("#{PUBLIC_PATH}#{path}index")
206
+ else
207
+ String.new("#{PUBLIC_PATH}#{path}")
208
+ end
209
+
210
+ unless path.end_with?(MEDIA_EXTENSIONS[content_type])
211
+ out_path << MEDIA_EXTENSIONS[content_type]
212
+ end
213
+
214
+ out_path
215
+ end
216
+
217
+ def guess_view_path
218
+ return if @path.instance_of?(Regexp)
219
+
220
+ view_path = String.new("#{LeanWeb::VIEW_PATH}#{@path}")
221
+ view_path << 'index' if @path[-1] == '/' # add index if is index
222
+ view_path.sub!(%r{\.[^/]+$}, '') # drop static file extension if set
223
+
224
+ Dir["#{view_path}.*"].first # return first file match or nil
183
225
  end
184
226
  end
185
227
  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.4.0'
13
+ end
data/lib/leanweb.rb CHANGED
@@ -2,14 +2,14 @@
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.2.0'
12
+ require_relative 'leanweb/version'
13
13
 
14
14
  ROOT_PATH = ENV.fetch('LEANWEB_ROOT_PATH', Dir.pwd)
15
15
  CONTROLLER_PATH = "#{ROOT_PATH}/src/controllers"
@@ -26,13 +26,11 @@ module LeanWeb
26
26
  'text/csv' => '.csv'
27
27
  }.freeze
28
28
 
29
+ DEFAULT_CONTROLLER = :MainController
30
+
29
31
  autoload :Route, 'leanweb/route.rb'
30
32
  autoload :Controller, 'leanweb/controller.rb'
31
33
  autoload :App, 'leanweb/app.rb'
32
34
  end
33
35
 
34
36
  require_relative 'leanweb/helpers'
35
-
36
- # TODO: Remove if/when tilt-emacs_org gets merged on a Tilt release.
37
- require 'tilt'
38
- Tilt.register_lazy(:EmacsOrgTemplate, 'tilt/emacs_org', 'org')
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leanweb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felix Freeman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-01 00:00:00.000000000 Z
11
+ date: 2022-11-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 2.0.10
33
+ version: 2.0.11
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 2.0.10
40
+ version: 2.0.11
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: haml
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 5.16.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-sprint
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.2.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.2.2
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rake
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,34 @@ dependencies:
122
136
  - - "~>"
123
137
  - !ruby/object:Gem::Version
124
138
  version: 0.20.1
139
+ - !ruby/object:Gem::Dependency
140
+ name: tilt-emacs_org
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.1.1
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.1.1
153
+ - !ruby/object:Gem::Dependency
154
+ name: webmock
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.18'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '3.18'
125
167
  - !ruby/object:Gem::Dependency
126
168
  name: yard
127
169
  requirement: !ruby/object:Gem::Requirement
@@ -145,14 +187,19 @@ extensions: []
145
187
  extra_rdoc_files: []
146
188
  files:
147
189
  - bin/leanweb
190
+ - contrib/lib/hawese.rb
191
+ - contrib/lib/lean_mail.rb
192
+ - contrib/lib/leanweb/controller_mixins/render_with_layout.rb
193
+ - contrib/lib/rack/session/json_file.rb
148
194
  - lib/leanweb.rb
149
195
  - lib/leanweb/app.rb
150
196
  - lib/leanweb/controller.rb
151
197
  - lib/leanweb/helpers.rb
152
198
  - lib/leanweb/route.rb
199
+ - lib/leanweb/version.rb
153
200
  homepage: https://leanweb.hacktivista.org
154
201
  licenses:
155
- - LicenseRef-LICENSE
202
+ - AGPL-3.0-only
156
203
  metadata:
157
204
  homepage_uri: https://leanweb.hacktivista.org
158
205
  source_code_uri: https://git.hacktivista.org/leanweb
@@ -163,6 +210,7 @@ post_install_message:
163
210
  rdoc_options: []
164
211
  require_paths:
165
212
  - lib
213
+ - contrib/lib
166
214
  required_ruby_version: !ruby/object:Gem::Requirement
167
215
  requirements:
168
216
  - - ">="
@@ -174,7 +222,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
222
  - !ruby/object:Gem::Version
175
223
  version: '0'
176
224
  requirements: []
177
- rubygems_version: 3.2.3
225
+ rubygems_version: 3.3.23
178
226
  signing_key:
179
227
  specification_version: 4
180
228
  summary: LeanWeb is a minimal hybrid static / dynamic web framework