radical 1.0.0

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: 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