radical 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/Gemfile +4 -2
- data/README.md +1 -1
- data/exe/rad +41 -0
- data/lib/radical/app.rb +64 -12
- data/lib/radical/asset.rb +24 -0
- data/lib/radical/asset_compiler.rb +40 -0
- data/lib/radical/assets.rb +45 -0
- data/lib/radical/controller.rb +52 -11
- data/lib/radical/database.rb +1 -1
- data/lib/radical/flash.rb +61 -0
- data/lib/radical/form.rb +73 -15
- data/lib/radical/generator/app/.env +5 -0
- data/lib/radical/generator/app/Gemfile +7 -0
- data/lib/radical/generator/app/app.rb +37 -0
- data/lib/radical/generator/app/config.ru +5 -0
- data/lib/radical/generator/app/controllers/controller.rb +4 -0
- data/lib/radical/generator/app/models/model.rb +4 -0
- data/lib/radical/generator/app/routes.rb +5 -0
- data/lib/radical/generator/blank_migration.rb +11 -0
- data/lib/radical/generator/controller.rb +59 -0
- data/lib/radical/generator/migration.rb +13 -0
- data/lib/radical/generator/model.rb +9 -0
- data/lib/radical/generator/views/_form.rb +6 -0
- data/lib/radical/generator/views/edit.rb +3 -0
- data/lib/radical/generator/views/index.rb +24 -0
- data/lib/radical/generator/views/new.rb +4 -0
- data/lib/radical/generator/views/show.rb +5 -0
- data/lib/radical/generator.rb +155 -0
- data/lib/radical/model.rb +1 -1
- data/lib/radical/router.rb +142 -43
- data/lib/radical/routes.rb +17 -6
- data/lib/radical/security_headers.rb +27 -0
- data/lib/radical/strings.rb +17 -0
- data/lib/radical/view.rb +17 -9
- data/lib/radical.rb +7 -0
- data/radical.gemspec +3 -2
- metadata +41 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3d14c9a576701105d60a08c6339a19aeebc080b6a4fd6f7235d8c5dec82ea287
|
4
|
+
data.tar.gz: b03f1d945e0f441401d99375062423dff70fa9c50cc394e470444d253faa46c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbdbdd7c8a1a56755ef772bbd7ce24a197111d97d44be0d1c1f6a56be24361d1f80469583f4969c0d8b8d9500ce92a66333e71539e8a79b845f249659b72a6ec
|
7
|
+
data.tar.gz: d3425aef0d2a3d87788b7169ba81b67b8c71faed045227aaff586a699ad46482d0770cfaacf0a738d787bb28b20be42c47b7d88ef79eda5632af5abe91da1e1b
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
|
7
7
|
# [Unreleased]
|
8
8
|
|
9
|
+
# 1.2.0 (2021-12-17)
|
10
|
+
|
11
|
+
- *Breaking* Support only one level of nested resources, make them shallow only
|
12
|
+
- *Breaking* Move `db/migrations` to `migrations`
|
13
|
+
- *Breaking* Change `<%== f.button 'Save' %>` to `<%== f.submit 'Save' %>`
|
14
|
+
- *Breaking* Change the form helpers from `model[name]` to just `name`, it's cleaner.
|
15
|
+
- Add `Radical.env.[development?|production?|test?]`
|
16
|
+
- Add `_path` support for nested resource routes
|
17
|
+
- Add a random secret to the session cookie on startup
|
18
|
+
- Add asset concatention + compression (no minification yet)
|
19
|
+
- Remove dependence on rack-flash3
|
20
|
+
- Add security headers
|
21
|
+
- Add session configuration with `session` method in `App`
|
22
|
+
- Move all `_path` method definitions to `Radical::Controller`
|
23
|
+
- Add `exe/rad`
|
24
|
+
- Add `rad g mvc Todo(s) name:text done_at:integer` generators
|
25
|
+
- Add attrs to form helpers
|
26
|
+
- Add `rad g app` generator
|
27
|
+
- Add migrate/rollback `rad` commands
|
28
|
+
|
9
29
|
# 1.1.0 (2021-12-06)
|
10
30
|
|
11
31
|
- *Breaking* `root`, `resource` and `resources` methods no longer take class constants
|
data/Gemfile
CHANGED
data/README.md
CHANGED
data/exe/rad
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/radical/generator'
|
4
|
+
require_relative '../lib/radical/database'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
@options = {}
|
8
|
+
|
9
|
+
OptionParser.new do |opts|
|
10
|
+
opts.on('-v', '--verbose', 'Show extra information') do
|
11
|
+
@options[:verbose] = true
|
12
|
+
end
|
13
|
+
|
14
|
+
opts.on('-h', '--help', 'Show help information') do
|
15
|
+
@options[:help] = true
|
16
|
+
end
|
17
|
+
end.parse!
|
18
|
+
|
19
|
+
generate = Radical::Generator.new(ARGV[2], ARGV.drop(3)) if %w[g generate].include?(ARGV.first)
|
20
|
+
|
21
|
+
case ARGV[0..1]
|
22
|
+
when %w[g mvc], %w[generate mvc]
|
23
|
+
generate.mvc
|
24
|
+
when %w[g model], %w[generate model]
|
25
|
+
generate.migration
|
26
|
+
generate.model
|
27
|
+
when %w[g controller], %w[generate controller]
|
28
|
+
generate.controller
|
29
|
+
when %w[g views], %w[generate views]
|
30
|
+
generate.views
|
31
|
+
when %w[g migration], %w[generate migration]
|
32
|
+
generate.migration(model: false)
|
33
|
+
when %w[g app]
|
34
|
+
generate.app
|
35
|
+
when %w[migrate]
|
36
|
+
Radical::Database.migrate!
|
37
|
+
when %w[rollback]
|
38
|
+
Radical::Database.rollback!
|
39
|
+
else
|
40
|
+
puts 'Command not supported'
|
41
|
+
end
|
data/lib/radical/app.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'securerandom'
|
3
4
|
require 'rack'
|
4
|
-
require 'rack/flash'
|
5
5
|
require 'rack/csrf'
|
6
6
|
|
7
|
-
require_relative '
|
7
|
+
require_relative 'asset'
|
8
|
+
require_relative 'assets'
|
9
|
+
require_relative 'asset_compiler'
|
8
10
|
require_relative 'env'
|
11
|
+
require_relative 'flash'
|
12
|
+
require_relative 'routes'
|
13
|
+
require_relative 'security_headers'
|
9
14
|
|
10
15
|
# The main entry point for a Radical application
|
11
16
|
#
|
@@ -34,7 +39,38 @@ module Radical
|
|
34
39
|
class App
|
35
40
|
class << self
|
36
41
|
def routes(route_class)
|
37
|
-
@routes
|
42
|
+
@routes = route_class
|
43
|
+
end
|
44
|
+
|
45
|
+
def assets(&block)
|
46
|
+
@assets = Assets.new
|
47
|
+
|
48
|
+
block.call(@assets)
|
49
|
+
end
|
50
|
+
|
51
|
+
def compile_assets
|
52
|
+
@assets.compile
|
53
|
+
end
|
54
|
+
|
55
|
+
def serve_assets
|
56
|
+
@serve_assets = true
|
57
|
+
end
|
58
|
+
|
59
|
+
def security_headers(headers = {})
|
60
|
+
@security_headers = headers
|
61
|
+
end
|
62
|
+
|
63
|
+
def session(options = {})
|
64
|
+
defaults = {
|
65
|
+
path: '/',
|
66
|
+
secret: session_secret,
|
67
|
+
http_only: true,
|
68
|
+
same_site: :lax,
|
69
|
+
secure: env.production?,
|
70
|
+
expire_after: 2_592_000 # 30 days
|
71
|
+
}
|
72
|
+
|
73
|
+
@session = defaults.merge(options)
|
38
74
|
end
|
39
75
|
|
40
76
|
def env
|
@@ -44,6 +80,10 @@ module Radical
|
|
44
80
|
def app
|
45
81
|
router = @routes.router
|
46
82
|
env = self.env
|
83
|
+
assets = @assets
|
84
|
+
serve_assets = @serve_assets
|
85
|
+
security_headers = @security_headers || {}
|
86
|
+
session = @session || self.session
|
47
87
|
|
48
88
|
@app ||= Rack::Builder.app do
|
49
89
|
use Rack::CommonLogger
|
@@ -51,23 +91,29 @@ module Radical
|
|
51
91
|
use Rack::Runtime
|
52
92
|
use Rack::MethodOverride
|
53
93
|
use Rack::ContentLength
|
54
|
-
use Rack::Deflater
|
55
94
|
use Rack::ETag
|
95
|
+
use Rack::Deflater
|
56
96
|
use Rack::Head
|
57
97
|
use Rack::ConditionalGet
|
58
98
|
use Rack::ContentType
|
59
|
-
use Rack::Session::Cookie,
|
60
|
-
secret: ENV['SESSION_SECRET'],
|
61
|
-
http_only: true,
|
62
|
-
same_site: :lax,
|
63
|
-
secure: env.production?,
|
64
|
-
expire_after: 2_592_000 # 30 days
|
99
|
+
use Rack::Session::Cookie, session
|
65
100
|
use Rack::Csrf, raise: env.development?, skip: router.routes.values.flatten.select { |a| a.is_a?(Class) }.uniq.map(&:skip_csrf_actions).flatten(1)
|
66
|
-
use
|
101
|
+
use Flash
|
102
|
+
use SecurityHeaders, security_headers
|
103
|
+
|
104
|
+
if serve_assets || env.development?
|
105
|
+
use Rack::Static, urls: ['/assets', '/public'],
|
106
|
+
header_rules: [
|
107
|
+
[/\.(?:css\.gz)$/, { 'Content-Type' => 'text/css', 'Content-Encoding' => 'gzip' }],
|
108
|
+
[/\.(?:js\.gz)$/, { 'Content-Type' => 'application/javascript', 'Content-Encoding' => 'gzip' }],
|
109
|
+
[/\.(?:css\.br)$/, { 'Content-Type' => 'text/css', 'Content-Encoding' => 'br' }],
|
110
|
+
[/\.(?:js\.br)$/, { 'Content-Type' => 'application/javascript', 'Content-Encoding' => 'br' }]
|
111
|
+
]
|
112
|
+
end
|
67
113
|
|
68
114
|
run lambda { |rack_env|
|
69
115
|
begin
|
70
|
-
router.route(Rack::Request.new(rack_env)).finish
|
116
|
+
router.route(Rack::Request.new(rack_env), options: { assets: assets }).finish
|
71
117
|
rescue ModelNotFound
|
72
118
|
raise unless env.production?
|
73
119
|
|
@@ -80,6 +126,12 @@ module Radical
|
|
80
126
|
def call(env)
|
81
127
|
app.call(env)
|
82
128
|
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def session_secret
|
133
|
+
@session_secret ||= (ENV['SESSION_SECRET'] || SecureRandom.hex(32))
|
134
|
+
end
|
83
135
|
end
|
84
136
|
end
|
85
137
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radical
|
4
|
+
class Asset
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def initialize(filename, path:)
|
8
|
+
@filename = filename
|
9
|
+
@path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
def full_path
|
13
|
+
File.join(path, @filename)
|
14
|
+
end
|
15
|
+
|
16
|
+
def content
|
17
|
+
File.read(full_path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def ext
|
21
|
+
File.extname(@filename)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'brotli'
|
4
|
+
require 'digest'
|
5
|
+
require 'zlib'
|
6
|
+
|
7
|
+
module Radical
|
8
|
+
class AssetCompiler
|
9
|
+
def self.gzip(filename, content)
|
10
|
+
# nil, 31 == support for gunzip
|
11
|
+
File.write(filename, Zlib::Deflate.new(nil, 31).deflate(content, Zlib::FINISH))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.compile(assets, path:, compressor: :none)
|
15
|
+
s = assets.map(&:content).join("\n")
|
16
|
+
ext = assets.first.ext
|
17
|
+
|
18
|
+
# hash the contents of each concatenated asset
|
19
|
+
hash = Digest::SHA1.hexdigest s
|
20
|
+
|
21
|
+
# use hash to bust the cache
|
22
|
+
name = "#{hash}#{ext}"
|
23
|
+
filename = File.join(path, name)
|
24
|
+
|
25
|
+
case compressor
|
26
|
+
when :gzip
|
27
|
+
name = "#{name}.gz"
|
28
|
+
gzip("#{filename}.gz", s)
|
29
|
+
when :brotli
|
30
|
+
name = "#{name}.br"
|
31
|
+
File.write("#{filename}.br", Brotli.deflate(s, mode: :text, quality: 11))
|
32
|
+
else
|
33
|
+
File.write(filename, s)
|
34
|
+
end
|
35
|
+
|
36
|
+
# output asset path for browser
|
37
|
+
"/assets/#{name}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radical
|
4
|
+
class Assets
|
5
|
+
attr_accessor :assets_path, :compiled, :assets
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@assets = {
|
9
|
+
css: [],
|
10
|
+
js: []
|
11
|
+
}
|
12
|
+
@compressor = :none
|
13
|
+
@assets_path = File.join(__dir__, 'assets')
|
14
|
+
@compiled = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def css(filenames)
|
18
|
+
@assets[:css] = filenames
|
19
|
+
end
|
20
|
+
|
21
|
+
def js(filenames)
|
22
|
+
@assets[:js] = filenames
|
23
|
+
end
|
24
|
+
|
25
|
+
def prepend_assets_path(path)
|
26
|
+
@assets_path = File.join(path, 'assets')
|
27
|
+
end
|
28
|
+
|
29
|
+
def brotli
|
30
|
+
@compressor = :brotli
|
31
|
+
end
|
32
|
+
|
33
|
+
def gzip
|
34
|
+
@compressor = :gzip
|
35
|
+
end
|
36
|
+
|
37
|
+
def compile
|
38
|
+
css = @assets[:css].map { |f| Asset.new(f, path: File.join(@assets_path, 'css')) }
|
39
|
+
js = @assets[:js].map { |f| Asset.new(f, path: File.join(@assets_path, 'js')) }
|
40
|
+
|
41
|
+
@compiled[:css] = AssetCompiler.compile(css, path: @assets_path, compressor: @compressor)
|
42
|
+
@compiled[:js] = AssetCompiler.compile(js, path: @assets_path, compressor: @compressor)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/radical/controller.rb
CHANGED
@@ -31,7 +31,7 @@ module Radical
|
|
31
31
|
|
32
32
|
sig { returns(String) }
|
33
33
|
def route_name
|
34
|
-
to_s.split('::').last.gsub(/Controller$/, '')
|
34
|
+
Strings.snake_case to_s.split('::').last.gsub(/Controller$/, '')
|
35
35
|
end
|
36
36
|
|
37
37
|
sig { params(actions: Symbol).void }
|
@@ -72,9 +72,12 @@ module Radical
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
|
-
|
76
|
-
|
75
|
+
attr_reader :options
|
76
|
+
|
77
|
+
sig { params(request: Rack::Request, options: T.nilable(Hash)).void }
|
78
|
+
def initialize(request, options: {})
|
77
79
|
@request = request
|
80
|
+
@options = options
|
78
81
|
end
|
79
82
|
|
80
83
|
sig { params(status: T.any(Symbol, Integer)).returns(Rack::Response) }
|
@@ -92,14 +95,14 @@ module Radical
|
|
92
95
|
@request.params
|
93
96
|
end
|
94
97
|
|
95
|
-
sig { params(name: T.any(String, Symbol)).returns(String) }
|
96
|
-
def view(name)
|
97
|
-
View.render(self.class.route_name, name, self)
|
98
|
+
sig { params(name: T.any(String, Symbol), locals: T.nilable(Hash)).returns(String) }
|
99
|
+
def view(name, locals = {})
|
100
|
+
View.render(self.class.route_name, name, self, { locals: locals })
|
98
101
|
end
|
99
102
|
|
100
|
-
sig { params(name: T.any(String, Symbol)).returns(String) }
|
101
|
-
def partial(name)
|
102
|
-
View.render(self.class.route_name, "_#{name}", self, layout: false)
|
103
|
+
sig { params(name: T.any(String, Symbol), locals: T.nilable(Hash)).returns(String) }
|
104
|
+
def partial(name, locals = {})
|
105
|
+
View.render(self.class.route_name, "_#{name}", self, { locals: locals, layout: false })
|
103
106
|
end
|
104
107
|
|
105
108
|
sig { params(options: Hash, block: T.proc.void).returns(String) }
|
@@ -130,17 +133,55 @@ module Radical
|
|
130
133
|
@request.env['rack.session']
|
131
134
|
end
|
132
135
|
|
136
|
+
def assets_path(type)
|
137
|
+
assets = options[:assets]
|
138
|
+
|
139
|
+
if Env.production?
|
140
|
+
compiled_assets_path(assets, type)
|
141
|
+
else
|
142
|
+
not_compiled_assets_path(assets, type)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def compiled_assets_path(assets, type)
|
147
|
+
if type == :css
|
148
|
+
link_tag(assets.compiled[:css])
|
149
|
+
else
|
150
|
+
script_tag(assets.compiled[:js])
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def not_compiled_assets_path(assets, type)
|
155
|
+
if type == :css
|
156
|
+
assets.assets[:css].map do |asset|
|
157
|
+
link_tag("/assets/#{type}/#{asset}")
|
158
|
+
end.join("\n")
|
159
|
+
else
|
160
|
+
assets.assets[:js].map do |asset|
|
161
|
+
script_tag("/assets/#{type}/#{asset}")
|
162
|
+
end.join("\n")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
133
166
|
private
|
134
167
|
|
135
168
|
def emit(tag)
|
136
|
-
@output =
|
169
|
+
@output = String.new if @output.nil?
|
137
170
|
@output << tag.to_s
|
138
171
|
end
|
139
172
|
|
140
173
|
def capture(block)
|
141
|
-
@output = eval
|
174
|
+
@output = eval '_buf', block.binding
|
142
175
|
yield
|
143
176
|
@output
|
144
177
|
end
|
178
|
+
|
179
|
+
def script_tag(src)
|
180
|
+
"<script type=\"application/javascript\" src=\"#{src}\"></script>"
|
181
|
+
end
|
182
|
+
|
183
|
+
def link_tag(href)
|
184
|
+
"<link rel=\"stylesheet\" type=\"text/css\" href=\"#{href}\" />"
|
185
|
+
end
|
145
186
|
end
|
146
187
|
end
|
data/lib/radical/database.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radical
|
4
|
+
class Flash
|
5
|
+
class SessionUnavailable < StandardError; end
|
6
|
+
|
7
|
+
SESSION_KEY = 'rack.session'
|
8
|
+
FLASH_KEY = '__FLASH__'
|
9
|
+
|
10
|
+
class FlashHash
|
11
|
+
def initialize(session)
|
12
|
+
raise SessionUnavailable, 'No session variable found. Requires Rack::Session' unless session
|
13
|
+
|
14
|
+
@session = session
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
hash[key] ||= session.delete(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def []=(key, value)
|
22
|
+
hash[key] = session[key] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def mark!
|
26
|
+
@flagged = session.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear!
|
30
|
+
@flagged.each { |k| session.delete(k) }
|
31
|
+
@flagged.clear
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def hash
|
37
|
+
@hash ||= {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def session
|
41
|
+
@session[FLASH_KEY] ||= {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(app)
|
46
|
+
@app = app
|
47
|
+
end
|
48
|
+
|
49
|
+
def call(env)
|
50
|
+
flash_hash ||= FlashHash.new(env[SESSION_KEY])
|
51
|
+
|
52
|
+
flash_hash.mark!
|
53
|
+
|
54
|
+
res = @app.call(env)
|
55
|
+
|
56
|
+
flash_hash.clear!
|
57
|
+
|
58
|
+
res
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/radical/form.rb
CHANGED
@@ -4,34 +4,63 @@ require 'rack/csrf'
|
|
4
4
|
|
5
5
|
module Radical
|
6
6
|
class Form
|
7
|
+
SELF_CLOSING_TAGS = %w[
|
8
|
+
area
|
9
|
+
base
|
10
|
+
br
|
11
|
+
col
|
12
|
+
embed
|
13
|
+
hr
|
14
|
+
img
|
15
|
+
input
|
16
|
+
keygen
|
17
|
+
link
|
18
|
+
meta
|
19
|
+
param
|
20
|
+
source
|
21
|
+
track
|
22
|
+
wbr
|
23
|
+
].freeze
|
24
|
+
|
7
25
|
def initialize(options, controller)
|
8
26
|
@model = options[:model]
|
9
27
|
@controller = controller
|
10
|
-
@
|
11
|
-
@override_method = options[:method]&.upcase || (@model.saved? ? 'PATCH' : 'POST')
|
28
|
+
@override_method = options[:method]&.upcase || (@model&.saved? ? 'PATCH' : 'POST')
|
12
29
|
@method = %w[GET POST].include?(@override_method) ? @override_method : 'POST'
|
30
|
+
@action = options[:action] || action_from(model: @model, controller: controller)
|
31
|
+
end
|
32
|
+
|
33
|
+
def text(name, attrs = {})
|
34
|
+
attrs.merge!(type: 'text', name: name, value: @model&.public_send(name))
|
13
35
|
|
14
|
-
|
15
|
-
@controller.public_send(:"#{@route_name}_path", @model)
|
16
|
-
else
|
17
|
-
@controller.public_send(:"#{@route_name}_path")
|
18
|
-
end
|
36
|
+
tag 'input', attrs
|
19
37
|
end
|
20
38
|
|
21
|
-
def
|
22
|
-
|
39
|
+
def number(name, attrs = {})
|
40
|
+
attrs.merge!(type: 'number', name: name, value: @model&.public_send(name))
|
41
|
+
|
42
|
+
tag 'input', attrs
|
23
43
|
end
|
24
44
|
|
25
|
-
def button(
|
26
|
-
|
45
|
+
def button(attrs = {}, &block)
|
46
|
+
tag 'button', attrs, &block
|
27
47
|
end
|
28
48
|
|
29
|
-
def submit(
|
30
|
-
|
49
|
+
def submit(value_or_attrs = {})
|
50
|
+
attrs = {}
|
51
|
+
|
52
|
+
case value_or_attrs
|
53
|
+
when String
|
54
|
+
attrs[:value] = value_or_attrs
|
55
|
+
when Hash
|
56
|
+
attrs = value_or_attrs || {}
|
57
|
+
end
|
58
|
+
|
59
|
+
tag 'input', attrs.merge('type' => 'submit')
|
31
60
|
end
|
32
61
|
|
33
62
|
def open_tag
|
34
|
-
"<form action
|
63
|
+
"<form #{html_attributes(action: @action, method: @method)}>"
|
35
64
|
end
|
36
65
|
|
37
66
|
def csrf_tag
|
@@ -39,11 +68,40 @@ module Radical
|
|
39
68
|
end
|
40
69
|
|
41
70
|
def rack_override_tag
|
42
|
-
|
71
|
+
attrs = { value: @override_method, type: 'hidden', name: '_method' }
|
72
|
+
|
73
|
+
tag('input', attrs) unless %w[GET POST].include?(@override_method)
|
43
74
|
end
|
44
75
|
|
45
76
|
def close_tag
|
46
77
|
'</form>'
|
47
78
|
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def tag(name, attrs, &block)
|
83
|
+
attr_string = attrs.empty? ? '' : " #{html_attributes(attrs)}"
|
84
|
+
open_tag = "<#{name}"
|
85
|
+
self_closing = SELF_CLOSING_TAGS.include?(name)
|
86
|
+
end_tag = self_closing ? ' />' : "</#{name}>"
|
87
|
+
|
88
|
+
"#{open_tag}#{attr_string}#{self_closing ? '' : '>'}#{block&.call}#{end_tag}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def html_attributes(options = {})
|
92
|
+
options.transform_keys(&:to_s).sort_by { |k, _| k }.map { |k, v| "#{k}=\"#{v}\"" }.join(' ')
|
93
|
+
end
|
94
|
+
|
95
|
+
def action_from(controller:, model:)
|
96
|
+
return if model.nil?
|
97
|
+
|
98
|
+
route_name = controller.class.route_name
|
99
|
+
|
100
|
+
if model.saved?
|
101
|
+
controller.send(:"#{route_name}_path", model)
|
102
|
+
else
|
103
|
+
controller.send(:"#{route_name}_path")
|
104
|
+
end
|
105
|
+
end
|
48
106
|
end
|
49
107
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'radical'
|
4
|
+
|
5
|
+
def require_all(*args)
|
6
|
+
args.each do |arg|
|
7
|
+
file = File.join(__dir__, arg)
|
8
|
+
|
9
|
+
if File.exist?("#{file}.rb")
|
10
|
+
require file
|
11
|
+
else
|
12
|
+
Dir[File.join(file, '*.rb')].sort.each do |f|
|
13
|
+
require f
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require_all(
|
20
|
+
'models/model',
|
21
|
+
'models',
|
22
|
+
'controllers/controller',
|
23
|
+
'controllers',
|
24
|
+
'routes'
|
25
|
+
)
|
26
|
+
|
27
|
+
# the main entry point into the application
|
28
|
+
class App < Radical::App
|
29
|
+
routes Routes
|
30
|
+
|
31
|
+
assets do |a|
|
32
|
+
a.css []
|
33
|
+
a.js []
|
34
|
+
end
|
35
|
+
|
36
|
+
compile_assets if Radical.env.production?
|
37
|
+
end
|