leanweb 0.2.0 → 0.3.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: d4e80c5813858b838b39fea56bb96b2fb28f7749d22f02ab04694662a8595d96
4
+ data.tar.gz: c2fdc8ebf92d647516db11352e5a6cb7bc36f1489dfabe69c095e7090f1b7970
5
5
  SHA512:
6
- metadata.gz: a3b3abccd00de5d76a06513c021b409d19073c911f3d236b7c7412c42e723bad9e616514a02e278e55271d5faf5709f94fb283af6fa93aa873e2144ff0d3211b
7
- data.tar.gz: a366dce05e7a8108e7b3ca4096112a3567e8082a0934e64d323401a938dceb24bfdc6c03dd53193827800b04a05c1c4c9ab53189cc45453e7925422dfb58535b
6
+ metadata.gz: 1bed8458a93a2f0139609f17fe3402f69065182607b6b6664cc0c6d90ab3815f4dca7cd08c165dab17e5448d9c3f7d3c44ef8e1f28468cd3e4c42a66b096c8b8
7
+ data.tar.gz: 320c619aedd70c9bd0ed5e698ef456e293d3cf754bbe3ca939a7669b45ad117a371089db15737edf0c5c271ad638b84784e6ee3a730093a82a16843828a1074c
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
 
@@ -0,0 +1,62 @@
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
32
+ uri = URI("#{ENDPOINT}/gateways/payment-methods/purchase")
33
+ JSON.parse(Net::HTTP.get(uri))
34
+ end
35
+
36
+ def gateway_schema(gateway, query_params, schema = 'purchase')
37
+ uri = URI("#{ENDPOINT}/gateways/#{gateway}/schemas/#{schema}")
38
+ uri.query = URI.encode_www_form(query_params)
39
+ JSON.parse(Net::HTTP.get(uri))
40
+ end
41
+
42
+ def purchase(gateway, fields)
43
+ fields[:origin] = ORIGIN
44
+ uri = URI("#{ENDPOINT}/gateways/#{gateway}/purchase")
45
+ uri.query = URI.encode_www_form(return_url: RETURN_URL)
46
+ response = Net::HTTP.post(
47
+ uri,
48
+ fields.to_json,
49
+ 'Content-Type' => 'application/json'
50
+ )
51
+ JSON.parse(response.body)
52
+ end
53
+
54
+ def purchase_from_schema(gateway, schema)
55
+ fields = {}
56
+ schema['properties'].each do |property, contents|
57
+ fields[property] = contents['default'] if contents.include?('default')
58
+ end
59
+ purchase(gateway, fields)
60
+ end
61
+ end
62
+ 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,170 @@
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
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: {},
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
+ protected
56
+
57
+ # @param path [String]
58
+ # @param layout [String]
59
+ # @param options [Hash] Check `Tilt::EmacsOrg` options.
60
+ # @option options [String] :setupfile can be absolute or relative to
61
+ # {VIEW_PATH}.
62
+ def render_org(path, layout: nil, options: {})
63
+ options[:setupfile] = "#{VIEW_PATH}/#{options[:setupfile]}" \
64
+ if options.include?(:setupfile) && options[:setupfile][0] != '/'
65
+
66
+ org_template = create_template(path, options)
67
+
68
+ @content_for[:title] = org_template.title \
69
+ unless @content_for.include?(:title)
70
+
71
+ if layout
72
+ create_template(layout).render(self){ org_template.render(self) }
73
+ else
74
+ org_template.render(self)
75
+ end
76
+ end
77
+
78
+ # @param path [String]
79
+ # @param layout [String]
80
+ # @param options [Hash] Check `RedCarpet` `render_options`.
81
+ # @option options [true] :toc To place an HTML based table of contents on
82
+ # `@content_for[:toc]` and render with option `:with_toc_data`.
83
+ def render_markdown(path, layout: nil, options: {})
84
+ maybe_render_markdown_toc!(path, options)
85
+ markdown_template = create_template(path, options)
86
+ if layout
87
+ create_template(layout).render(self){ markdown_template.render(self) }
88
+ else
89
+ markdown_template.render(self)
90
+ end
91
+ end
92
+
93
+ # If `options[:toc]` is set, deletes options `:toc` and adds
94
+ # `:with_toc_data`. Also adds `@content_for[:toc]`.
95
+ def maybe_render_markdown_toc!(path, options)
96
+ return unless options.include?(:toc)
97
+
98
+ require('redcarpet')
99
+ @content_for[:toc] = create_template(
100
+ path, { renderer: Redcarpet::Render::HTML_TOC }
101
+ ).render
102
+
103
+ options.delete(:toc)
104
+ options[:with_toc_data] = true
105
+ end
106
+
107
+ # @param path [String]
108
+ # @param layout [String]
109
+ # @param options [Hash] Check `Haml` options.
110
+ # @yield If block given.
111
+ def render_other(path, layout: nil, options: {})
112
+ template = create_template(path, options)
113
+ if layout
114
+ create_template(layout).render(self) do
115
+ template.render(self){ yield if block_given? }
116
+ end
117
+ else
118
+ template.render(self){ yield if block_given? }
119
+ end
120
+ end
121
+
122
+ def render_by_extension(path, layout, options, &block)
123
+ case File.extname(path)
124
+ when '.org'
125
+ render_org(path, layout: layout, options: options)
126
+ when '.md'
127
+ render_markdown(path, layout: layout, options: options)
128
+ else
129
+ render_other(path, layout: layout, options: options, &block)
130
+ end
131
+ end
132
+
133
+ def prepare_head(js: nil, jsm: nil, css: nil, raw: nil)
134
+ head = String.new
135
+ head << prepare_head_js(js) unless js.nil?
136
+ head << prepare_head_js(jsm, 'module') unless jsm.nil?
137
+ head << prepare_head_css(css) unless css.nil?
138
+ head << prepare_head_raw(raw) unless raw.nil?
139
+ @content_for[:head] = head
140
+ end
141
+
142
+ def prepare_head_js(js, type = 'application/javascript')
143
+ head = String.new
144
+ js = [js] if js.instance_of?(String)
145
+ js.each do |src|
146
+ head << "<script type='#{type}' src='#{src}'"
147
+ head << ' defer' if type != 'module'
148
+ head << "></script>\n"
149
+ end
150
+ head
151
+ end
152
+
153
+ def prepare_head_css(css)
154
+ head = String.new
155
+ css = [css] if css.instance_of?(String)
156
+ css.each do |src|
157
+ head << "<link rel='stylesheet' type='text/css' href='#{src}'>\n"
158
+ end
159
+ head
160
+ end
161
+
162
+ def prepare_head_raw(raw)
163
+ head = String.new
164
+ raw = [raw] if raw.instance_of?(String)
165
+ raw.each{ |src| head << "#{src}\n" }
166
+ head
167
+ end
168
+ end
169
+ end
170
+ 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
@@ -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
@@ -108,42 +110,45 @@ module LeanWeb
108
110
 
109
111
  # Assign value to `@action`.
110
112
  def action=(value)
111
- @action = if value.instance_of?(Proc)
112
- value
113
- else
114
- Action.new(**prepare_action_hash(value))
115
- end
113
+ @action =
114
+ if value.instance_of?(Proc)
115
+ value
116
+ else
117
+ Action.new(**prepare_action_hash(value))
118
+ end
116
119
  end
117
120
 
118
121
  # @param src_value [Hash, String, nil] Check {#initialize} action param for
119
122
  # valid input.
120
123
  # @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
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
135
128
  value
136
129
  end
137
130
 
138
- def default_action_name
139
- "#{path_basename.gsub('-', '_')}_#{@method.downcase}"
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
140
145
  end
141
146
 
142
147
  # @param request [Rack::Request]
143
148
  # @return [Array] a valid Rack response.
144
149
  def respond_method(request)
145
150
  params = action_params(request.path)
146
- require_relative("#{LeanWeb::CONTROLLER_PATH}/#{@action.file}")
151
+ require_relative("#{CONTROLLER_PATH}/#{@action.controller.to_s.snakeize}")
147
152
  controller = Object.const_get(@action.controller).new(self, request)
148
153
  return controller.public_send(@action.action, **params) \
149
154
  if params.instance_of?(Hash)
@@ -155,9 +160,12 @@ module LeanWeb
155
160
  # @return [Array] a valid Rack response.
156
161
  def respond_proc(request)
157
162
  params = action_params(request.path)
158
- 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)
159
167
 
160
- @action.call(*params)
168
+ controller.instance_exec(*params, &@action)
161
169
  end
162
170
 
163
171
  # @param request_path [String]
@@ -178,8 +186,18 @@ module LeanWeb
178
186
  # @param content_type [String]
179
187
  # @return [String] absolute route to path + extension based on content_type.
180
188
  def output_path(path, content_type)
181
- path += 'index' if path[-1] == '/'
182
- "#{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
183
201
  end
184
202
  end
185
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,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.3.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-10-14 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.21
178
226
  signing_key:
179
227
  specification_version: 4
180
228
  summary: LeanWeb is a minimal hybrid static / dynamic web framework