leanweb 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []