leanweb 0.2.0 → 0.4.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 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