radical 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 537aa5f33439e08833e0aca3ff572a0e017c20cdbfaa3d267c303191cc692fd0
4
+ data.tar.gz: 95ee9c628803bf81f237d15aa6c3d92a1d0c55f4412809eb4e74a57dd510e15c
5
+ SHA512:
6
+ metadata.gz: f30a37e86df78f5beb7a345b96031a1d83779a723629a7b1ca3c70223d7cea94acfc277bea77d32b8461382fbed2f146b3bf69a732a77224fd4d586f7877811d
7
+ data.tar.gz: a322aef87deae2b114842c4a88ac31fc02f35258701770d9a2017158616a6d07229a5960de8879efbd403b286d4c1a53ec64ba7f9b7e202570734f8b9de0c5b1
data/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ Style/FrozenStringLiteralComment:
2
+ Enabled: false
3
+ Style/Documentation:
4
+ Enabled: false
data/.solargraph.yml ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ include:
3
+ - "**/*.rb"
4
+ exclude:
5
+ - spec/**/*
6
+ - test/**/*
7
+ - vendor/**/*
8
+ - ".bundle/**/*"
9
+ reporters:
10
+ - rubocop
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5
+
6
+ ## [Unreleased]
7
+
8
+ - Very basic understanding of how much memory it takes for a basic ruby app
9
+ - Start to integrate controllers and routing
10
+ - Add url params support to router
11
+ - Simple (not efficient) loop through routes and compare regex routing
12
+ - Bring in rack as a dependency in the gemspec
13
+ - Actually make everything work with rack for real
14
+ - Add an examples folder to test it out with real http requests!
15
+ - Add very simple ERB views
16
+ - Experiment with tilt
17
+ - Better view stuff, no tilt yet
18
+ - Use Rack::Request and Rack::Response
19
+ - Automatically render view from controller if a Rack::Response isn't returned
20
+ - Update tests
21
+ - Experiment with a module based mvc thing, didn't pan out
22
+ - Only allow resource routes, custom method names will not be found
23
+ - Add a bunch of rack middleware
24
+ - Move call to class
25
+ - Use Rack::Test
26
+ - Rename routes to resources; add resource method
27
+ - Add nested resources, multi-argument resources
data/Dockerfile ADDED
@@ -0,0 +1,34 @@
1
+ FROM docker.io/library/ruby:slim
2
+
3
+ RUN apt-get update -qq
4
+ RUN apt-get install -y build-essential libsqlite3-dev git-core
5
+
6
+ RUN apt-get install -y --no-install-recommends libjemalloc2
7
+ RUN rm -rf /var/lib/apt/lists/*
8
+
9
+ ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
10
+
11
+ ARG USER=app
12
+ ARG GROUP=app
13
+ ARG UID=1101
14
+ ARG GID=1101
15
+
16
+ RUN groupadd --gid $GID $GROUP
17
+ RUN useradd --uid $UID --gid $GID --groups $GROUP -ms /bin/bash $USER
18
+
19
+ RUN mkdir -p /var/app
20
+ RUN chown -R $USER:$GROUP /var/app
21
+
22
+ USER $USER
23
+ WORKDIR /var/app
24
+
25
+ ENV BUNDLER_VERSION='2.2.27'
26
+ RUN gem install bundler --no-document -v '2.2.27'
27
+
28
+ COPY --chown=$USER Gemfile* /var/app/
29
+ COPY --chown=$USER *.gemspec /var/app/
30
+ RUN bundle install
31
+
32
+ COPY --chown=$USER . /var/app
33
+
34
+ CMD ["rake"]
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Sean Walker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # radical
2
+
3
+ A rails inspired ruby web framework
4
+
5
+ # Learning
6
+
7
+ Check out the `examples/` folder for some ideas on how to get started and when you're ready, check the `docs/` folder for more details.
8
+
9
+ # Quickstart
10
+
11
+ Create a directory with a `config.ru` and a `Gemfile` file inside of it
12
+
13
+ ```sh
14
+ mkdir your_project
15
+ cd your_project
16
+ touch config.ru Gemfile
17
+ ```
18
+
19
+ Put this inside of the `config.ru`
20
+
21
+ ```rb
22
+ require 'radical'
23
+
24
+ class Home < Radical::Controller
25
+ def index
26
+ plain '/'
27
+ end
28
+ end
29
+
30
+ class App < Radical::App
31
+ root Home
32
+ end
33
+
34
+ run App
35
+ ```
36
+
37
+ Install the gems and start the server
38
+
39
+ ```sh
40
+ gem install radical puma
41
+ puma
42
+ ```
43
+
44
+ Test it out
45
+
46
+ ```sh
47
+ curl localhost:9292
48
+ # => /
49
+ ```
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = FileList['test/**/test*.rb']
8
+ end
9
+
10
+ desc 'Run tests'
11
+ task default: :test
@@ -0,0 +1,103 @@
1
+ require 'rack'
2
+ require 'rack/flash'
3
+ require 'rack/csrf'
4
+
5
+ require_relative 'router'
6
+ require_relative 'env'
7
+
8
+ # The main entry point for a Radical application
9
+ #
10
+ # Example:
11
+ #
12
+ # class App < Radical::App
13
+ # root Home
14
+ # end
15
+ #
16
+ # App.call(
17
+ # {
18
+ # 'PATH_INFO' => '/',
19
+ # 'REQUEST_METHOD' => 'GET'
20
+ # }
21
+ # )
22
+ #
23
+ # Dispatches to:
24
+ #
25
+ # class Controller < Radical::Controller
26
+ # # GET /
27
+ # def index
28
+ # head :ok
29
+ # end
30
+ # end
31
+ module Radical
32
+ class App
33
+ class << self
34
+ def root(klass)
35
+ router.add_root(klass)
36
+ end
37
+
38
+ def resource(klass)
39
+ router.add_actions(klass, actions: Router::RESOURCE_ACTIONS)
40
+ end
41
+
42
+ def resources(*classes, &block)
43
+ prefix = "#{router.route_prefix(@parents)}/" if instance_variable_defined?(:@parents)
44
+
45
+ router.add_routes(classes, prefix: prefix)
46
+
47
+ return unless block
48
+
49
+ @parents ||= []
50
+ @parents << classes.last
51
+ block.call
52
+ end
53
+
54
+ def env
55
+ Env
56
+ end
57
+
58
+ def app
59
+ router = self.router
60
+ env = self.env
61
+
62
+ @app ||= Rack::Builder.app do
63
+ use Rack::CommonLogger
64
+ use Rack::ShowExceptions if env.development?
65
+ use Rack::Runtime
66
+ use Rack::MethodOverride
67
+ use Rack::ContentLength
68
+ use Rack::Deflater
69
+ use Rack::ETag
70
+ use Rack::Head
71
+ use Rack::ConditionalGet
72
+ use Rack::ContentType
73
+ use Rack::Session::Cookie, path: '/',
74
+ secret: ENV['SESSION_SECRET'],
75
+ http_only: true,
76
+ same_site: :lax,
77
+ secure: env.production?,
78
+ expire_after: 2_592_000 # 30 days
79
+ use Rack::Csrf, raise: env.development?, skip: router.routes.values.flatten.select { |a| a.is_a?(Class) }.uniq.map(&:skip_csrf_actions).flatten(1)
80
+ use Rack::Flash, sweep: true
81
+
82
+ run lambda { |rack_env|
83
+ begin
84
+ router.route(Rack::Request.new(rack_env)).finish
85
+ rescue ModelNotFound
86
+ raise unless env.production?
87
+
88
+ Rack::Response.new('404 Not Found', 404).finish
89
+ end
90
+ }
91
+ end
92
+ end
93
+
94
+ def router
95
+ @router ||= Router.new
96
+ end
97
+
98
+ def call(env)
99
+ app.call(env)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,144 @@
1
+ # typed: true
2
+ require 'rack/utils'
3
+ require 'rack/request'
4
+ require 'rack/response'
5
+ require 'sorbet-runtime'
6
+ require_relative 'view'
7
+ require_relative 'env'
8
+ require_relative 'form'
9
+
10
+ module Radical
11
+ class Controller
12
+ extend T::Sig
13
+
14
+ attr_accessor :request
15
+
16
+ class << self
17
+ extend T::Sig
18
+
19
+ attr_accessor :skip_csrf_actions
20
+
21
+ sig { params(path: String).void }
22
+ def prepend_view_path(path)
23
+ View.path path
24
+ end
25
+
26
+ def layout(name)
27
+ View.layout name
28
+ end
29
+
30
+ sig { returns(String) }
31
+ def route_name
32
+ to_s.split('::').last.gsub(/Controller$/, '').gsub(/([A-Z])/, '_\1')[1..-1].downcase
33
+ end
34
+
35
+ sig { params(actions: Symbol).void }
36
+ def skip_csrf(*actions)
37
+ @skip_csrf_actions = [] if @skip_csrf_actions.nil?
38
+
39
+ actions.each do |action|
40
+ @skip_csrf_actions << "#{action_to_http_method(action)}:#{action_to_url(action)}"
41
+ end
42
+ end
43
+
44
+ sig { params(action: Symbol).returns(String) }
45
+ def action_to_url(action)
46
+ case action
47
+ when :index, :create
48
+ "/#{route_name}"
49
+ when :show, :update, :destroy
50
+ "/#{route_name}/:id"
51
+ when :new
52
+ "/#{route_name}/new"
53
+ when :edit
54
+ "/#{route_name}/:id/edit"
55
+ end
56
+ end
57
+
58
+ sig { params(action: Symbol).returns(String) }
59
+ def action_to_http_method(action)
60
+ case action
61
+ when :index, :show, :new, :edit
62
+ 'GET'
63
+ when :create
64
+ 'POST'
65
+ when :update
66
+ 'PATCH'
67
+ when :destroy
68
+ 'DELETE'
69
+ end
70
+ end
71
+ end
72
+
73
+ sig { params(request: Rack::Request).void }
74
+ def initialize(request)
75
+ @request = request
76
+ end
77
+
78
+ sig { params(status: T.any(Symbol, Integer)).returns(Rack::Response) }
79
+ def head(status)
80
+ Rack::Response.new(nil, Rack::Utils::SYMBOL_TO_STATUS_CODE[status])
81
+ end
82
+
83
+ sig { params(body: String).returns(Rack::Response) }
84
+ def plain(body)
85
+ Rack::Response.new(body, 200, { 'Content-Type' => 'text/plain' })
86
+ end
87
+
88
+ sig { returns(Hash) }
89
+ def params
90
+ @request.params
91
+ end
92
+
93
+ sig { params(name: T.any(String, Symbol)).returns(String) }
94
+ def view(name)
95
+ View.render(self.class.route_name, name, self)
96
+ end
97
+
98
+ sig { params(name: T.any(String, Symbol)).returns(String) }
99
+ def partial(name)
100
+ View.render(self.class.route_name, "_#{name}", self, layout: false)
101
+ end
102
+
103
+ sig { params(options: Hash, block: T.proc.void).returns(String) }
104
+ def form(options, &block)
105
+ f = Form.new(options, self)
106
+
107
+ capture(block) do
108
+ emit f.open_tag
109
+ emit f.csrf_tag
110
+ emit f.rack_override_tag
111
+ yield f
112
+ emit f.close_tag
113
+ end
114
+ end
115
+
116
+ sig { params(to: T.any(Symbol, String)).returns(Rack::Response) }
117
+ def redirect(to)
118
+ to = self.class.action_to_url(to) if to.is_a?(Symbol)
119
+
120
+ Rack::Response.new(nil, 302, { 'Location' => to })
121
+ end
122
+
123
+ def flash
124
+ @request.env['rack.session']['__FLASH__']
125
+ end
126
+
127
+ def session
128
+ @request.env['rack.session']
129
+ end
130
+
131
+ private
132
+
133
+ def emit(tag)
134
+ @output = '' if @output.nil?
135
+ @output << tag.to_s
136
+ end
137
+
138
+ def capture(block)
139
+ @output = eval('_buf', block.binding)
140
+ yield
141
+ @output
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,92 @@
1
+ require 'sqlite3'
2
+ require_relative 'table'
3
+
4
+ module Radical
5
+ class Database
6
+ class << self
7
+ attr_accessor :connection, :migrations_path
8
+
9
+ def db
10
+ connection
11
+ end
12
+
13
+ def migrate!
14
+ @migrate = true
15
+
16
+ db.execute 'create table if not exists radical_migrations ( version integer primary key )'
17
+
18
+ pending_migrations.each do |migration|
19
+ puts "Executing migration #{migration}"
20
+ sql = eval File.read(migration)
21
+ db.execute sql
22
+ db.execute 'insert into radical_migrations (version) values (?)', [version(migration)]
23
+ end
24
+ end
25
+
26
+ def rollback!
27
+ @rollback = true
28
+
29
+ db.execute 'create table if not exists radical_migrations ( version integer primary key )'
30
+
31
+ migration = applied_migrations.last
32
+
33
+ puts "Rolling back migration #{migration}"
34
+
35
+ sql = eval File.read(migration)
36
+ db.execute sql
37
+ db.execute 'delete from radical_migrations where version = ?', [version(migration)]
38
+ end
39
+
40
+ def applied_versions
41
+ sql = 'select * from radical_migrations order by version'
42
+ rows = db.execute sql
43
+
44
+ rows.map { |r| r['version'] }
45
+ end
46
+
47
+ def applied_migrations
48
+ migrations.select { |f| applied_versions.include?(version(f)) }
49
+ end
50
+
51
+ def pending_migrations
52
+ migrations.reject { |f| applied_versions.include?(version(f)) }
53
+ end
54
+
55
+ def migrations
56
+ Dir[File.join(migrations_path, 'db', 'migrations', '*.rb')].sort
57
+ end
58
+
59
+ def version(filename)
60
+ filename.split(File::SEPARATOR).last.split('_').first.to_i
61
+ end
62
+
63
+ def migration(&block)
64
+ block.call
65
+ end
66
+
67
+ def change(&block)
68
+ @change = true
69
+
70
+ block.call
71
+ end
72
+
73
+ def up(&block)
74
+ block.call
75
+ end
76
+
77
+ def down(&block)
78
+ block.call
79
+ end
80
+
81
+ def create_table(name, &block)
82
+ return "drop table #{name}" if @change && @rollback
83
+
84
+ table = Table.new(name)
85
+
86
+ block.call(table)
87
+
88
+ "create table #{name} ( id integer primary key, #{table.columns.join(',')} )"
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,23 @@
1
+ # typed: true
2
+
3
+ module Radical
4
+ class Env
5
+ class << self
6
+ def radical_env
7
+ ENV['RADICAL_ENV'] || ''
8
+ end
9
+
10
+ def development?
11
+ radical_env.downcase == 'development'
12
+ end
13
+
14
+ def test?
15
+ radical_env.downcase == 'test'
16
+ end
17
+
18
+ def production?
19
+ radical_env.downcase == 'production'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,47 @@
1
+ require 'rack/csrf'
2
+
3
+ module Radical
4
+ class Form
5
+ def initialize(options, controller)
6
+ @model = options[:model]
7
+ @controller = controller
8
+ @route_name = @controller.class.route_name
9
+ @override_method = options[:method]&.upcase || (@model.saved? ? 'PATCH' : 'POST')
10
+ @method = %w[GET POST].include?(@override_method) ? @override_method : 'POST'
11
+
12
+ @action = if @model.saved?
13
+ @controller.public_send(:"#{@route_name}_path", @model)
14
+ else
15
+ @controller.public_send(:"#{@route_name}_path")
16
+ end
17
+ end
18
+
19
+ def text(name)
20
+ "<input type=text name=#{@route_name}[#{name}] value=\"#{@model.public_send(name)}\" />"
21
+ end
22
+
23
+ def button(name)
24
+ "<button type=submit>#{name}</button>"
25
+ end
26
+
27
+ def submit(value)
28
+ "<input type=submit value=#{value} />"
29
+ end
30
+
31
+ def open_tag
32
+ "<form action=#{@action} method=#{@method}>"
33
+ end
34
+
35
+ def csrf_tag
36
+ Rack::Csrf.tag(@controller.request.env)
37
+ end
38
+
39
+ def rack_override_tag
40
+ "<input type=hidden name=_method value=#{@override_method} />" unless %w[GET POST].include?(@override_method)
41
+ end
42
+
43
+ def close_tag
44
+ '</form>'
45
+ end
46
+ end
47
+ end