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
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
<<~RB
|
4
|
+
# frozen_string_literal: true
|
5
|
+
|
6
|
+
class #{plural_constant} < Controller
|
7
|
+
def index
|
8
|
+
@#{plural} = #{singular_constant}.all
|
9
|
+
end
|
10
|
+
|
11
|
+
def show; end
|
12
|
+
|
13
|
+
def new
|
14
|
+
@#{singular} = #{singular_constant}.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
@#{singular} = #{singular_constant}.new(#{singular}_params)
|
19
|
+
|
20
|
+
if @#{singular}.save
|
21
|
+
flash[:success] = '#{singular_constant} created'
|
22
|
+
redirect #{plural}_path
|
23
|
+
else
|
24
|
+
render :new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def edit; end
|
29
|
+
|
30
|
+
def update
|
31
|
+
if #{singular}.update(#{singular}_params)
|
32
|
+
flash[:success] = '#{singular_constant} updated'
|
33
|
+
redirect #{plural}_path
|
34
|
+
else
|
35
|
+
render :edit
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def destroy
|
40
|
+
if #{singular}.destroy
|
41
|
+
flash[:success] = '#{singular_constant} destroyed'
|
42
|
+
else
|
43
|
+
flash[:error] = 'Error destroying #{singular_constant}'
|
44
|
+
end
|
45
|
+
|
46
|
+
redirect #{plural}_path
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def #{singular}_params
|
52
|
+
params['#{singular}'].slice(#{params})
|
53
|
+
end
|
54
|
+
|
55
|
+
def #{singular}
|
56
|
+
@#{singular} = #{singular_constant}.find(params['id'])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
RB
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<<~ERB
|
2
|
+
<a href="<%= new_#{plural}_path %>">New #{singular}</a>
|
3
|
+
|
4
|
+
<table>
|
5
|
+
<thead>
|
6
|
+
#{th(leading: 6)}
|
7
|
+
<th></th>
|
8
|
+
</thead>
|
9
|
+
<tbody>
|
10
|
+
<% @#{plural}.each do |#{singular}| %>
|
11
|
+
<tr>
|
12
|
+
#{td(leading: 10)}
|
13
|
+
<td>
|
14
|
+
<a href="<%= #{plural}_path(#{singular}) %>">show</a>
|
15
|
+
<a href="<%= edit_#{plural}_path(#{singular}) %>">edit</a>
|
16
|
+
<%== form model: #{singular}, method: :delete do |f| %>
|
17
|
+
<%== f.submit 'delete' %>
|
18
|
+
<% end %>
|
19
|
+
</td>
|
20
|
+
</tr>
|
21
|
+
<% end %>
|
22
|
+
</tbody>
|
23
|
+
</table>
|
24
|
+
ERB
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Radical
|
7
|
+
class Generator
|
8
|
+
def initialize(name, props)
|
9
|
+
@name = name
|
10
|
+
@props = props
|
11
|
+
end
|
12
|
+
|
13
|
+
def mvc
|
14
|
+
migration
|
15
|
+
model
|
16
|
+
views
|
17
|
+
controller
|
18
|
+
end
|
19
|
+
|
20
|
+
def migration(model: true)
|
21
|
+
dir = File.join(Dir.pwd, 'migrations')
|
22
|
+
FileUtils.mkdir_p dir
|
23
|
+
|
24
|
+
template = instance_eval File.read(File.join(__dir__, 'generator', "#{model ? '' : 'blank_'}migration.rb"))
|
25
|
+
migration_name = model ? "#{Time.now.to_i}_create_table_#{plural}.rb" : "#{Time.now.to_i}_#{@name}.rb"
|
26
|
+
filename = File.join(dir, migration_name)
|
27
|
+
|
28
|
+
write(filename, template)
|
29
|
+
end
|
30
|
+
|
31
|
+
def model
|
32
|
+
template = instance_eval File.read File.join(__dir__, 'generator', 'model.rb')
|
33
|
+
dir = File.join(Dir.pwd, 'models')
|
34
|
+
FileUtils.mkdir_p dir
|
35
|
+
filename = File.join(dir, "#{singular}.rb")
|
36
|
+
|
37
|
+
write(filename, template)
|
38
|
+
end
|
39
|
+
|
40
|
+
def controller
|
41
|
+
template = instance_eval File.read File.join(__dir__, 'generator', 'controller.rb')
|
42
|
+
dir = File.join(Dir.pwd, 'controllers')
|
43
|
+
FileUtils.mkdir_p dir
|
44
|
+
filename = File.join(dir, "#{plural}.rb")
|
45
|
+
|
46
|
+
write(filename, template)
|
47
|
+
end
|
48
|
+
|
49
|
+
def views
|
50
|
+
dir = File.join(Dir.pwd, 'views', plural)
|
51
|
+
FileUtils.mkdir_p dir
|
52
|
+
|
53
|
+
Dir[File.join(__dir__, 'generator', 'views', '*.rb')].sort.each do |template|
|
54
|
+
contents = instance_eval File.read template
|
55
|
+
filename = File.join(dir, "#{File.basename(template, '.rb')}.erb")
|
56
|
+
|
57
|
+
write(filename, contents)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def app
|
62
|
+
@name = nil if @name == '.'
|
63
|
+
parts = [Dir.pwd, @name].compact
|
64
|
+
dir = File.join(*parts)
|
65
|
+
FileUtils.mkdir_p dir
|
66
|
+
|
67
|
+
%w[
|
68
|
+
assets/css
|
69
|
+
assets/js
|
70
|
+
controllers
|
71
|
+
migrations
|
72
|
+
models
|
73
|
+
views
|
74
|
+
].each do |dir_|
|
75
|
+
puts "Creating directory #{dir_}"
|
76
|
+
FileUtils.mkdir_p File.join(dir, dir_)
|
77
|
+
end
|
78
|
+
|
79
|
+
Dir[File.join(__dir__, 'generator', 'app', '**', '*.*')].sort.each do |template|
|
80
|
+
contents = File.read(template)
|
81
|
+
filename = File.join(dir, File.path(template).gsub("#{__dir__}/generator/app/", ''))
|
82
|
+
|
83
|
+
write(filename, contents)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Explicitly include .env
|
87
|
+
template = File.join(__dir__, 'generator', 'app', '.env')
|
88
|
+
contents = instance_eval File.read(template)
|
89
|
+
filename = File.join(dir, '.env')
|
90
|
+
write(filename, contents)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def write(filename, contents)
|
96
|
+
if File.exist?(filename)
|
97
|
+
puts "Skipped #{File.basename(filename)}"
|
98
|
+
else
|
99
|
+
File.write(filename, contents)
|
100
|
+
puts "Created #{filename}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def singular_constant
|
105
|
+
@name.gsub(/\(.*\)/, '')
|
106
|
+
end
|
107
|
+
|
108
|
+
def plural_constant
|
109
|
+
@name.gsub(/[)(]/, '')
|
110
|
+
end
|
111
|
+
|
112
|
+
def singular
|
113
|
+
Strings.camel_case singular_constant
|
114
|
+
end
|
115
|
+
|
116
|
+
def plural
|
117
|
+
Strings.camel_case plural_constant
|
118
|
+
end
|
119
|
+
|
120
|
+
def columns(leading:)
|
121
|
+
@props
|
122
|
+
.map { |p| p.split(':') }
|
123
|
+
.map { |name, type| "t.#{type} #{name}" }
|
124
|
+
.join "#{' ' * leading}\n"
|
125
|
+
end
|
126
|
+
|
127
|
+
def th(leading:)
|
128
|
+
@props
|
129
|
+
.map { |p| p.split(':').first }
|
130
|
+
.map { |name| "<th>#{name}</th>" }
|
131
|
+
.join "#{' ' * leading}\n"
|
132
|
+
end
|
133
|
+
|
134
|
+
def td(leading:)
|
135
|
+
@props
|
136
|
+
.map { |p| p.split(':').first }
|
137
|
+
.map { |name| "<td><%= #{singular}.#{name} %></td>" }
|
138
|
+
.join "#{' ' * leading}\n"
|
139
|
+
end
|
140
|
+
|
141
|
+
def inputs(leading:)
|
142
|
+
@props
|
143
|
+
.map { |p| p.split(':').first }
|
144
|
+
.map { |name| "<%== f.text :#{name} %>" }
|
145
|
+
.join "#{' ' * leading}\n"
|
146
|
+
end
|
147
|
+
|
148
|
+
def params
|
149
|
+
@props
|
150
|
+
.map { |p| p.split(':').first }
|
151
|
+
.map { |name| "'#{name}'" }
|
152
|
+
.join ', '
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/radical/model.rb
CHANGED
@@ -49,7 +49,7 @@ module Radical
|
|
49
49
|
def initialize(params = {})
|
50
50
|
columns.each do |column|
|
51
51
|
self.class.attr_accessor column.to_sym
|
52
|
-
instance_variable_set "@#{column}", params[column]
|
52
|
+
instance_variable_set "@#{column}", (params[column] || params[column.to_sym])
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
data/lib/radical/router.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'rack'
|
5
5
|
require 'sorbet-runtime'
|
@@ -47,6 +47,8 @@ module Radical
|
|
47
47
|
].freeze
|
48
48
|
|
49
49
|
RESOURCE_ACTIONS = [
|
50
|
+
[:new, 'GET', '/new'],
|
51
|
+
[:create, 'POST', ''],
|
50
52
|
[:show, 'GET', ''],
|
51
53
|
[:edit, 'GET', '/edit'],
|
52
54
|
[:update, 'PUT', ''],
|
@@ -68,54 +70,31 @@ module Radical
|
|
68
70
|
|
69
71
|
sig { params(klass: Class).void }
|
70
72
|
def add_root(klass)
|
71
|
-
|
73
|
+
add_routes(klass, name: '', actions: ACTIONS)
|
74
|
+
add_root_paths(klass)
|
72
75
|
end
|
73
76
|
|
74
|
-
sig { params(klass: Class
|
75
|
-
def
|
76
|
-
|
77
|
-
|
78
|
-
actions.each do |method, http_method, suffix|
|
79
|
-
next unless klass.method_defined?(method)
|
80
|
-
|
81
|
-
path = "/#{prefix}#{name}#{suffix}"
|
82
|
-
path = Regexp.new("^#{path.gsub(/:(\w+)/, '(?<\1>[a-zA-Z0-9_]+)')}$").freeze
|
83
|
-
|
84
|
-
if %i[index create show update destroy].include?(method) && !klass.method_defined?(:"#{klass.route_name}_path")
|
85
|
-
klass.define_method :"#{klass.route_name}_path" do |obj = nil|
|
86
|
-
if obj.is_a?(Model)
|
87
|
-
"/#{klass.route_name}/#{obj.id}"
|
88
|
-
else
|
89
|
-
"/#{klass.route_name}"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
if method == :new
|
95
|
-
klass.define_method :"new_#{klass.route_name}_path" do
|
96
|
-
"/#{klass.route_name}/new"
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
if method == :edit
|
101
|
-
klass.define_method :"edit_#{klass.route_name}_path" do |obj|
|
102
|
-
"/#{klass.route_name}/#{obj.id}/edit"
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
@routes[http_method] << [path, [klass, method]]
|
107
|
-
end
|
77
|
+
sig { params(klass: Class).void }
|
78
|
+
def add_resource(klass)
|
79
|
+
add_routes(klass, actions: RESOURCE_ACTIONS)
|
80
|
+
add_resource_paths(klass)
|
108
81
|
end
|
109
82
|
|
110
|
-
sig { params(
|
111
|
-
def
|
112
|
-
|
113
|
-
|
83
|
+
sig { params(klass: Class, parents: T.nilable(T::Array[Class])).void }
|
84
|
+
def add_resources(klass, parents: nil)
|
85
|
+
if parents
|
86
|
+
parents.each do |scope|
|
87
|
+
add_routes(klass, actions: ACTIONS, scope: scope)
|
88
|
+
add_resources_paths(klass, scope: scope)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
add_routes(klass, actions: ACTIONS)
|
92
|
+
add_resources_paths(klass)
|
114
93
|
end
|
115
94
|
end
|
116
95
|
|
117
|
-
sig { params(request: Rack::Request).returns(Rack::Response) }
|
118
|
-
def route(request)
|
96
|
+
sig { params(request: Rack::Request, options: T.nilable(Hash)).returns(Rack::Response) }
|
97
|
+
def route(request, options: {})
|
119
98
|
params = T.let({}, T.nilable(Hash))
|
120
99
|
|
121
100
|
route = @routes[request.request_method].find do |r|
|
@@ -130,7 +109,7 @@ module Radical
|
|
130
109
|
request.update_param(k, v)
|
131
110
|
end
|
132
111
|
|
133
|
-
instance = klass.new(request)
|
112
|
+
instance = klass.new(request, options: options)
|
134
113
|
|
135
114
|
response = instance.public_send(method)
|
136
115
|
|
@@ -142,5 +121,125 @@ module Radical
|
|
142
121
|
|
143
122
|
Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' })
|
144
123
|
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
sig { params(klass: Class, actions: Array, name: T.nilable(String), scope: T.nilable(Class)).void }
|
128
|
+
def add_routes(klass, actions:, name: nil, scope: nil)
|
129
|
+
name ||= klass.route_name
|
130
|
+
|
131
|
+
actions.each do |method, http_method, suffix|
|
132
|
+
next unless klass.method_defined?(method)
|
133
|
+
|
134
|
+
path = if scope
|
135
|
+
if %i[index new create].include?(method)
|
136
|
+
"/#{scope.route_name}/:#{scope.route_name}_id/#{name}#{suffix}"
|
137
|
+
else
|
138
|
+
"/#{name}#{suffix}"
|
139
|
+
end
|
140
|
+
else
|
141
|
+
"/#{name}#{suffix}"
|
142
|
+
end
|
143
|
+
|
144
|
+
path = Regexp.new("^#{path.gsub(/:(\w+)/, '(?<\1>[a-zA-Z0-9_]+)')}$").freeze
|
145
|
+
|
146
|
+
@routes[http_method] << [path, [klass, method]]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
sig { params(klass: Class).void }
|
151
|
+
def add_root_paths(klass)
|
152
|
+
route_name = klass.route_name
|
153
|
+
|
154
|
+
if %i[index create show update destroy].any? { |method| klass.method_defined?(method) }
|
155
|
+
Controller.define_method :"#{route_name}_path" do |obj = nil|
|
156
|
+
if obj
|
157
|
+
"/#{obj.id}"
|
158
|
+
else
|
159
|
+
'/'
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
if klass.method_defined?(:new)
|
165
|
+
Controller.define_method :"new_#{route_name}_path" do
|
166
|
+
'/new'
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
return unless klass.method_defined?(:edit)
|
171
|
+
|
172
|
+
Controller.define_method :"edit_#{route_name}_path" do |obj|
|
173
|
+
"/#{obj.id}/edit"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
sig { params(klass: Class).void }
|
178
|
+
def add_resource_paths(klass)
|
179
|
+
name = klass.route_name
|
180
|
+
|
181
|
+
if %i[create show update destroy].any? { |method| klass.method_defined?(method) }
|
182
|
+
Controller.define_method :"#{name}_path" do
|
183
|
+
"/#{name}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
if klass.method_defined?(:new)
|
188
|
+
Controller.define_method :"new_#{name}_path" do
|
189
|
+
"/#{name}/new"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
return unless klass.method_defined?(:edit)
|
194
|
+
|
195
|
+
Controller.define_method :"edit_#{name}_path" do
|
196
|
+
"/#{name}/edit"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
sig { params(klass: Class, scope: T.nilable(Class)).void }
|
201
|
+
def add_resources_paths(klass, scope: nil)
|
202
|
+
path_name = klass.route_name
|
203
|
+
scope_path_name = [scope&.route_name, klass.route_name].compact.join('_')
|
204
|
+
name = klass.route_name
|
205
|
+
|
206
|
+
if %i[index create show update destroy].any? { |method| klass.method_defined?(method) }
|
207
|
+
if scope
|
208
|
+
Controller.define_method :"#{scope_path_name}_path" do |parent|
|
209
|
+
"/#{scope.route_name}/#{parent.id}/#{name}"
|
210
|
+
end
|
211
|
+
|
212
|
+
Controller.define_method :"#{path_name}_path" do |obj|
|
213
|
+
"/#{name}/#{obj.id}"
|
214
|
+
end
|
215
|
+
else
|
216
|
+
Controller.define_method :"#{path_name}_path" do |obj = nil|
|
217
|
+
if obj
|
218
|
+
"/#{name}/#{obj.id}"
|
219
|
+
else
|
220
|
+
"/#{name}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
if klass.method_defined?(:new)
|
227
|
+
if scope
|
228
|
+
Controller.define_method :"new_#{scope_path_name}_path" do |parent|
|
229
|
+
"/#{scope.route_name}/#{parent.id}/#{name}/new"
|
230
|
+
end
|
231
|
+
else
|
232
|
+
Controller.define_method :"new_#{path_name}_path" do
|
233
|
+
"/#{name}/new"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
return unless klass.method_defined?(:edit)
|
239
|
+
|
240
|
+
Controller.define_method :"edit_#{path_name}_path" do |obj|
|
241
|
+
"/#{name}/#{obj.id}/edit"
|
242
|
+
end
|
243
|
+
end
|
145
244
|
end
|
146
245
|
end
|
data/lib/radical/routes.rb
CHANGED
@@ -13,6 +13,10 @@ module Radical
|
|
13
13
|
@router ||= Router.new
|
14
14
|
end
|
15
15
|
|
16
|
+
def parents
|
17
|
+
@parents ||= []
|
18
|
+
end
|
19
|
+
|
16
20
|
sig { params(name: T.any(String, Symbol)).void }
|
17
21
|
def root(name)
|
18
22
|
klass = Object.const_get(name)
|
@@ -25,7 +29,7 @@ module Radical
|
|
25
29
|
classes = names.map { |c| Object.const_get(c) }
|
26
30
|
|
27
31
|
classes.each do |klass|
|
28
|
-
router.
|
32
|
+
router.add_resource(klass)
|
29
33
|
end
|
30
34
|
end
|
31
35
|
|
@@ -33,14 +37,21 @@ module Radical
|
|
33
37
|
def resources(*names, &block)
|
34
38
|
classes = names.map { |c| Object.const_get(c) }
|
35
39
|
|
36
|
-
|
37
|
-
|
38
|
-
|
40
|
+
classes.each do |klass|
|
41
|
+
if parents.any?
|
42
|
+
router.add_resources(klass, parents: @parents)
|
43
|
+
|
44
|
+
# only one level of nesting
|
45
|
+
@parents = []
|
46
|
+
else
|
47
|
+
router.add_resources(klass)
|
48
|
+
end
|
49
|
+
end
|
39
50
|
|
40
51
|
return unless block
|
41
52
|
|
42
|
-
@parents
|
43
|
-
|
53
|
+
@parents = classes
|
54
|
+
|
44
55
|
block.call
|
45
56
|
end
|
46
57
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radical
|
4
|
+
class SecurityHeaders
|
5
|
+
DEFAULT_HEADERS = {
|
6
|
+
'X-Content-Type-Options' => 'nosniff',
|
7
|
+
'X-Frame-Options' => 'deny',
|
8
|
+
'X-XSS-Protection' => '1; mode=block',
|
9
|
+
'X-Permitted-Cross-Domain-Policies' => 'none',
|
10
|
+
'Strict-Transport-Security' => 'max-age=31536000;, max-age=31536000; includeSubdomains',
|
11
|
+
'Content-Security-Policy' => "default-src 'none'; style-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; font-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'; block-all-mixed-content;"
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def initialize(app, headers)
|
15
|
+
@app = app
|
16
|
+
@headers = DEFAULT_HEADERS.merge(headers)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
@app.call(env).tap do |_, headers|
|
21
|
+
@headers.each do |k, v|
|
22
|
+
headers[k] ||= v
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Radical
|
4
|
+
class Strings
|
5
|
+
SNAKE_CASE_REGEX = /\B([A-Z])/.freeze
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def snake_case(str)
|
9
|
+
str.gsub(SNAKE_CASE_REGEX, '_\1').downcase
|
10
|
+
end
|
11
|
+
|
12
|
+
def camel_case(str)
|
13
|
+
str.split('_').map(&:capitalize).join
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|