leanweb 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f0437a01908da014f67d9ff669d421e9334233e4e30563ae4b3d77ebb8c7385c
4
+ data.tar.gz: f7e810131f740cc6357ec039b4b246c650169db78ee593a88f3377e7f594a342
5
+ SHA512:
6
+ metadata.gz: b155fa1e345bac7b955a9c026c7a4eabab5166646fa59d731224234aa3fe2f702298de18d63eb37d8d7a8939857998c9465405002868b34909d04f690bd0c099
7
+ data.tar.gz: 76784d5a2b39cd6539bdd444ff57bbe346d0eb2a36c363dbf6e6349726aee0b0662e4300d6999790a92d8742d732426dfcec6b4505fe081daa8c73a6979995e5
data/bin/leanweb ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2022 Felix Freeman <libsys@hacktivista.org>
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/>.
9
+
10
+ # frozen_string_literal: true
11
+
12
+ require 'fileutils'
13
+ require 'leanweb'
14
+
15
+ # Script for building a new project.
16
+
17
+ files = [
18
+ {
19
+ filename: 'Gemfile',
20
+ content: <<~RUBY
21
+ # frozen_string_literal: true
22
+
23
+ source 'https://rubygems.org'
24
+
25
+ gem 'leanweb', '~> #{LeanWeb::VERSION}'
26
+ RUBY
27
+ }, {
28
+ filename: 'config.ru',
29
+ content: <<~RUBY
30
+ # frozen_string_literal: true
31
+
32
+ require 'leanweb'
33
+
34
+ if ENV['RACK_ENV'] == 'development'
35
+ use(Rack::Reloader, 0)
36
+ use(Rack::Static, urls: [''], root: 'public', cascade: true)
37
+ end
38
+
39
+ app = LeanWeb::App.init
40
+ run app
41
+ RUBY
42
+ }, {
43
+ filename: 'routes.rb',
44
+ content: <<~RUBY
45
+ # frozen_string_literal: true
46
+
47
+ [{ path: '/' }]
48
+ RUBY
49
+ }, {
50
+ filename: 'Rakefile',
51
+ content: <<~RUBY
52
+ # frozen_string_literal: true
53
+
54
+ require 'leanweb'
55
+
56
+ task default: %w[build]
57
+
58
+ task :build do
59
+ LeanWeb::App.init.build_static
60
+ end
61
+ RUBY
62
+ }, {
63
+ filename: 'src/controllers/main.rb',
64
+ content: <<~RUBY
65
+ # frozen_string_literal: true
66
+
67
+ require 'leanweb'
68
+
69
+ # Main controller is the default controller
70
+ class MainController < LeanWeb::Controller
71
+ def index_get
72
+ render 'index.haml'
73
+ end
74
+ end
75
+ RUBY
76
+ }, {
77
+ filename: 'src/views/index.haml',
78
+ content: <<~HAML
79
+ !!!
80
+ %html
81
+ %head
82
+ %meta{charset: "utf-8"}
83
+ %meta{name: "viewport", content: "width=device-width, initial-scale=1"}
84
+ %title LeanWeb framework
85
+ %body
86
+ %h1 It works!
87
+ HAML
88
+ }
89
+ ]
90
+
91
+ if !(1..2).include?(ARGV.size) || ARGV[0] != 'new'
92
+ puts 'Usage: leanweb new [directory]'
93
+ exit(1)
94
+ end
95
+
96
+ begin
97
+ path = ARGV[1] || '.'
98
+ FileUtils.mkdir_p(path)
99
+ FileUtils.cd(path)
100
+ FileUtils.mkdir_p(['public', 'src/controllers', 'src/views'])
101
+ files.each { |file| File.write(file[:filename], file[:content]) }
102
+ `git init`
103
+ puts("Project '#{File.basename(Dir.getwd)}' successfully created.")
104
+ rescue # rubocop:disable Style/RescueStandardError
105
+ puts('Woops! Something went wrong.')
106
+ raise
107
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright 2022 Felix Freeman <libsys@hacktivista.org>
2
+ #
3
+ # This file is part of "LeanWeb" and licensed under the terms of the Hacktivista
4
+ # General Public License version 0.1 or (at your option) any later version. You
5
+ # should have received a copy of this license along with the software. If not,
6
+ # see <https://hacktivista.org/licenses/>.
7
+
8
+ # frozen_string_literal: true
9
+
10
+ require 'rack'
11
+
12
+ module LeanWeb
13
+ # App resolves and builds static files from routes.
14
+ class App
15
+ attr_accessor :routes
16
+
17
+ # @param routes [Hash] Hash of routes with {Route} attributes.
18
+ def initialize(routes)
19
+ @routes = routes
20
+ end
21
+
22
+ # Entry point for dynamic routes (Rack).
23
+ # @param env [Hash] `env` for Rack.
24
+ def call(env)
25
+ request = Rack::Request.new(env)
26
+ route = @routes.find do |r|
27
+ (r[:method] || 'GET') == request.request_method && begin
28
+ r[:path] =~ request.path
29
+ rescue TypeError
30
+ r[:path] == request.path
31
+ end
32
+ end
33
+ return [404, {}, ['Not found']] unless route_exists?(route)
34
+
35
+ Route.new(**route).respond(request)
36
+ end
37
+
38
+ # Build static routes to files.
39
+ def build_static
40
+ @routes.each do |route|
41
+ route = Route.new(**route)
42
+ next unless route.static
43
+
44
+ begin
45
+ route.static.each { |seed| route.build(route.seed_path(seed)) }
46
+ rescue NoMethodError
47
+ route.build
48
+ end
49
+ end
50
+ end
51
+
52
+ # Initialize by evaluating the routes file.
53
+ # Do this here so users don't freak out for using eval and rubocop is happy
54
+ # on client side.
55
+ # @param file [String] Routes file.
56
+ def self.init(file = 'routes.rb')
57
+ new(eval(File.read(file))) # rubocop:disable Security/Eval
58
+ end
59
+
60
+ protected
61
+
62
+ # Check if route exists.
63
+ # If not on development environment, serve only dynamic routes.
64
+ # @param route [Hash]
65
+ def route_exists?(route)
66
+ return false if route.nil? \
67
+ || (ENV['RACK_ENV'] != 'development' && route[:static] != false)
68
+
69
+ true
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,89 @@
1
+ # Copyright 2022 Felix Freeman <libsys@hacktivista.org>
2
+ #
3
+ # This file is part of "LeanWeb" and licensed under the terms of the Hacktivista
4
+ # General Public License version 0.1 or (at your option) any later version. You
5
+ # should have received a copy of this license along with the software. If not,
6
+ # see <https://hacktivista.org/licenses/>.
7
+
8
+ # frozen_string_literal: true
9
+
10
+ require 'rack'
11
+
12
+ module LeanWeb
13
+ # Controller is a base controller with `@route`, `@request` and `@response`
14
+ # private attributes that will be shared with your views when you {render}
15
+ # Haml documents.
16
+ #
17
+ # Even if you don't {render}, you can use the `.finish` method from
18
+ # `Rack::Response` on `@response` to return a proper `Rack` response.
19
+ class Controller
20
+ # @param route [Route]
21
+ # @param request [Rack::Request]
22
+ def initialize(route, request = nil)
23
+ @route = route
24
+ @request = request
25
+ @response = Rack::Response.new(nil, 200)
26
+ end
27
+
28
+ # Render magic. Supports dynamic (and static) Haml documents, other
29
+ # documents don't support dynamic data.
30
+ #
31
+ # Depending on {Route#path} and {#render} `path` you will render different
32
+ # documents:
33
+ #
34
+ # - {Route#path} `/a/b/c` and `render('d')` will render `src/views/a/b/d`
35
+ # - {Route#path} `/a/b/c/` and `render('d')` will render `src/views/a/b/c/d`
36
+ # - `render('~/custom')` will render `src/views/custom`.
37
+ # - `render('/custom')` will render `/custom`.
38
+ #
39
+ # @param path [String] Might be an absolute path or a relative path to
40
+ # `src/views/` + the parent path of the last non-capture-group on
41
+ # `@route.path`. You can also make it relative to {LeanWeb::VIEW_PATH} by
42
+ # prepending `~`.
43
+ # @param content_type [String] Defaults to the proper Content-Type for file
44
+ # extension, `text/plain` on unknown files.
45
+ #
46
+ # @return [Rack::Request#finish] A valid rack response.
47
+ def render(path, content_type = nil) # rubocop:disable Metrics/MethodLength
48
+ path = absolute_view_path(path)
49
+ content = File.read(path)
50
+ case File.extname(path)
51
+ when '.haml'
52
+ require('haml')
53
+ @response.write(Haml::Engine.new(content).render(binding))
54
+ @response.set_header('Content-Type', content_type || 'text/html')
55
+ else
56
+ require('erb')
57
+ @response.write(ERB.new(content).result(binding))
58
+ @response.set_header('Content-Type', content_type || 'text/plain')
59
+ end
60
+ @response.finish
61
+ end
62
+
63
+ protected
64
+
65
+ # @param relative_path [String]
66
+ # @return [String] Full path.
67
+ def absolute_view_path(relative_path)
68
+ case relative_path[0]
69
+ when '/'
70
+ relative_path
71
+ when '~'
72
+ relative_path.sub('~', LeanWeb::VIEW_PATH)
73
+ else
74
+ "#{view_path_by_route_path}/#{relative_path}"
75
+ end
76
+ end
77
+
78
+ def view_path_by_route_path
79
+ LeanWeb::VIEW_PATH +
80
+ if @route.path.instance_of?(Regexp)
81
+ File.dirname(@route.str_path.sub(%r{/?\(.*}, ''))
82
+ elsif @route.path[-1] != '/'
83
+ File.dirname(@route.path)
84
+ else
85
+ @route.path.chomp('/')
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,164 @@
1
+ # Copyright 2022 Felix Freeman <libsys@hacktivista.org>
2
+ #
3
+ # This file is part of "LeanWeb" and licensed under the terms of the Hacktivista
4
+ # General Public License version 0.1 or (at your option) any later version. You
5
+ # should have received a copy of this license along with the software. If not,
6
+ # see <https://hacktivista.org/licenses/>.
7
+
8
+ # frozen_string_literal: true
9
+
10
+ module LeanWeb
11
+ # Action for {Route#action}.
12
+ Action = Struct.new(:file, :controller, :action, keyword_init: true)
13
+
14
+ # A single route which routes with the {#respond} method. It can also {#build}
15
+ # an static file.
16
+ class Route
17
+ attr_reader :path, :method, :action, :static
18
+
19
+ # A new instance of Route.
20
+ #
21
+ # @param path [String, Regexp] Path matcher, can be an String or Regexp with
22
+ # positional or named capture groups, `@action` will receive these as
23
+ # positional or named arguments.
24
+ # @param method [String, nil] Must be an HTTP verb such as `GET` or `POST`.
25
+ # @param action [Proc, Hash, String, nil] References a Method/Proc to be
26
+ # triggered. It can be:
27
+ #
28
+ # - A full hash `{ file: 'val', controller: 'val', action: 'val' }`.
29
+ # - A hash with `{ 'file' => 'action' }` only (which has a controller
30
+ # class name `{File}Controller`).
31
+ # - A simple string (which will consider file `main.rb` and controller
32
+ # `MainController`). Defaults to "{path_basename}_{method}", (ex:
33
+ # `index_get`).
34
+ # - It can also be a `Proc`.
35
+ #
36
+ # @param static [Boolean|Array] Defines a route as static. Set to `false` to
37
+ # say it can only work dynamically. You can also supply an array of arrays
38
+ # or hashes to generate static files based on that positional or keyword
39
+ # params.
40
+ def initialize(path:, method: 'GET', action: nil, static: true)
41
+ @path = path
42
+ @method = method
43
+ self.action = action
44
+ @static = static
45
+ end
46
+
47
+ # Last identifier on a path, returns `index` for `/`.
48
+ def path_basename
49
+ str_path[-1] == '/' ? 'index' : File.basename(str_path)
50
+ end
51
+
52
+ # @param request [Rack::Request]
53
+ # @return a valid rack response.
54
+ def respond(request)
55
+ return respond_proc(request) if @action.instance_of?(Proc)
56
+
57
+ respond_method(request)
58
+ end
59
+
60
+ # String path, independent if {path} is Regexp or String.
61
+ def str_path
62
+ @path.source.gsub(/[\^$]/, '')
63
+ rescue NoMethodError
64
+ @path
65
+ end
66
+
67
+ # On Regexp paths, return a string valid for making a request to this route.
68
+ # @param seed [Array, Hash] Seeds to use as replacement on capture groups.
69
+ # @return [String] sown path.
70
+ def seed_path(seed)
71
+ sown_path = str_path
72
+ if seed.instance_of?(Hash)
73
+ seed.each { |key, val| sown_path.sub!(/\(\?<#{key}>[^)]+\)/, val) }
74
+ else
75
+ seed.each { |val| sown_path.sub!(/\([^)]+\)/, val) }
76
+ end
77
+ sown_path
78
+ end
79
+
80
+ # Build this route as an static file and place it relative to
81
+ # {LeanWeb::PUBLIC_PATH}.
82
+ # @param request_path [String] Request path for dynamic (regex) routes.
83
+ def build(request_path = @path)
84
+ response = respond(
85
+ Rack::Request.new(
86
+ { 'PATH_INFO' => request_path, 'REQUEST_METHOD' => 'GET' }
87
+ )
88
+ )
89
+ out_path = output_path(request_path, response[1]['Content-Type'] || nil)
90
+ FileUtils.mkdir_p(File.dirname(out_path))
91
+
92
+ File.write(out_path, response[2][0])
93
+ end
94
+
95
+ protected
96
+
97
+ # Assign value to `@action`.
98
+ def action=(value)
99
+ @action = if value.instance_of?(Proc)
100
+ value
101
+ else
102
+ Action.new(**prepare_action_hash(value))
103
+ end
104
+ end
105
+
106
+ # @param value [Hash, String, nil] Check {#initialize} action param for
107
+ # valid input.
108
+ # @return [Hash] valid hash for {Action}.
109
+ def prepare_action_hash(value)
110
+ begin
111
+ value[:file], value[:action] = value.first \
112
+ unless %i[file controller action].include?(value.keys.first)
113
+ rescue NoMethodError
114
+ value = { action: value }
115
+ end
116
+ value[:file] ||= 'main'
117
+ value[:controller] ||= "#{value[:file].capitalize}Controller"
118
+ value[:action] ||= "#{path_basename}_#{@method.downcase}"
119
+ value
120
+ end
121
+
122
+ # @param request [Rack::Request]
123
+ # @return a valid rack response.
124
+ def respond_method(request)
125
+ params = action_params(request.path)
126
+ require_relative("#{LeanWeb::CONTROLLER_PATH}/#{@action.file}")
127
+ controller = Object.const_get(@action.controller).new(self, request)
128
+ return controller.public_send(@action.action, **params)\
129
+ if params.instance_of?(Hash)
130
+
131
+ controller.public_send(@action.action, *params)
132
+ end
133
+
134
+ # @param request [Rack::Request]
135
+ # @return a valid rack response.
136
+ def respond_proc(request)
137
+ params = action_params(request.path)
138
+ return @action.call(**params) if params.instance_of?(Hash)
139
+
140
+ @action.call(*params)
141
+ end
142
+
143
+ # @param request_path [String]
144
+ # @return [Array, Hash]
145
+ def action_params(request_path)
146
+ return nil unless @path.instance_of?(Regexp)
147
+
148
+ matches = @path.match(request_path)
149
+ return matches.named_captures.transform_keys(&:to_sym)\
150
+ if matches.named_captures != {}
151
+
152
+ matches.captures
153
+ end
154
+
155
+ # Output path for public file.
156
+ # @param path [String]
157
+ # @param content_type [String]
158
+ # @return [String] absolute route to path + extension based on content_type.
159
+ def output_path(path, content_type)
160
+ path += 'index' if path[-1] == '/'
161
+ "#{LeanWeb::PUBLIC_PATH}#{path}#{LeanWeb::MEDIA_EXTENSIONS[content_type]}"
162
+ end
163
+ end
164
+ end
data/lib/leanweb.rb ADDED
@@ -0,0 +1,30 @@
1
+ # Copyright 2022 Felix Freeman <libsys@hacktivista.org>
2
+ #
3
+ # This file is part of "LeanWeb" and licensed under the terms of the Hacktivista
4
+ # General Public License version 0.1 or (at your option) any later version. You
5
+ # should have received a copy of this license along with the software. If not,
6
+ # see <https://hacktivista.org/licenses/>.
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # LeanWeb is a minimal hybrid static / dynamic web framework.
11
+ module LeanWeb
12
+ VERSION = '0.1.1'
13
+
14
+ ROOT_PATH = ENV['LEANWEB_ROOT_PATH'] || Dir.pwd
15
+ CONTROLLER_PATH = "#{ROOT_PATH}/src/controllers"
16
+ VIEW_PATH = "#{ROOT_PATH}/src/views"
17
+ PUBLIC_PATH = "#{ROOT_PATH}/public"
18
+
19
+ MEDIA_EXTENSIONS = {
20
+ nil => '.html',
21
+ 'application/javascript' => '.js',
22
+ 'application/json' => '.json',
23
+ 'text/html' => '.html',
24
+ 'text/plain' => '.txt'
25
+ }.freeze
26
+
27
+ autoload :Route, 'leanweb/route.rb'
28
+ autoload :Controller, 'leanweb/controller.rb'
29
+ autoload :App, 'leanweb/app.rb'
30
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: leanweb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Felix Freeman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-01-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: haml
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - libsys@hacktivista.org
114
+ executables:
115
+ - leanweb
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - bin/leanweb
120
+ - lib/leanweb.rb
121
+ - lib/leanweb/app.rb
122
+ - lib/leanweb/controller.rb
123
+ - lib/leanweb/route.rb
124
+ homepage: https://git.hacktivista.org/leanweb
125
+ licenses:
126
+ - LicenseRef-LICENSE
127
+ metadata:
128
+ homepage_uri: https://git.hacktivista.org/leanweb
129
+ source_code_uri: https://git.hacktivista.org/leanweb
130
+ rubygems_mfa_required: 'true'
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 2.5.0
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubygems_version: 3.2.3
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: LeanWeb is a minimal hybrid static / dynamic web framework
150
+ test_files: []