radical 1.1.0 → 1.2.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 +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
|