radical 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/Gemfile +4 -2
  4. data/README.md +1 -1
  5. data/exe/rad +41 -0
  6. data/lib/radical/app.rb +64 -12
  7. data/lib/radical/asset.rb +24 -0
  8. data/lib/radical/asset_compiler.rb +40 -0
  9. data/lib/radical/assets.rb +45 -0
  10. data/lib/radical/controller.rb +52 -11
  11. data/lib/radical/database.rb +1 -1
  12. data/lib/radical/flash.rb +61 -0
  13. data/lib/radical/form.rb +73 -15
  14. data/lib/radical/generator/app/.env +5 -0
  15. data/lib/radical/generator/app/Gemfile +7 -0
  16. data/lib/radical/generator/app/app.rb +37 -0
  17. data/lib/radical/generator/app/config.ru +5 -0
  18. data/lib/radical/generator/app/controllers/controller.rb +4 -0
  19. data/lib/radical/generator/app/models/model.rb +4 -0
  20. data/lib/radical/generator/app/routes.rb +5 -0
  21. data/lib/radical/generator/blank_migration.rb +11 -0
  22. data/lib/radical/generator/controller.rb +59 -0
  23. data/lib/radical/generator/migration.rb +13 -0
  24. data/lib/radical/generator/model.rb +9 -0
  25. data/lib/radical/generator/views/_form.rb +6 -0
  26. data/lib/radical/generator/views/edit.rb +3 -0
  27. data/lib/radical/generator/views/index.rb +24 -0
  28. data/lib/radical/generator/views/new.rb +4 -0
  29. data/lib/radical/generator/views/show.rb +5 -0
  30. data/lib/radical/generator.rb +155 -0
  31. data/lib/radical/model.rb +1 -1
  32. data/lib/radical/router.rb +142 -43
  33. data/lib/radical/routes.rb +17 -6
  34. data/lib/radical/security_headers.rb +27 -0
  35. data/lib/radical/strings.rb +17 -0
  36. data/lib/radical/view.rb +17 -9
  37. data/lib/radical.rb +7 -0
  38. data/radical.gemspec +3 -2
  39. metadata +41 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a2836f72234086fb3981b282d57216a2d324ecdd2f14e3b38a1ce57b2757be3
4
- data.tar.gz: 14de9ad7784177b1ee837c9b12d2888ffdf50ea268bbcdffc0f3d6a54238a312
3
+ metadata.gz: 3d14c9a576701105d60a08c6339a19aeebc080b6a4fd6f7235d8c5dec82ea287
4
+ data.tar.gz: b03f1d945e0f441401d99375062423dff70fa9c50cc394e470444d253faa46c4
5
5
  SHA512:
6
- metadata.gz: 15fd6c727f86820b1644a984d3d125ed02d65aa7aed4f1cfb292a216fda7a6d9b2f232bb680c9ab05417bddf410c6a75495dce74edd2fc278f7a9b7589d0da4b
7
- data.tar.gz: 82c51dc592446f8a4492ba36e11e947aa20a8f937ae78b541d952a4156a88fac25b25eaec3d2de583a88a42cccd5de18297ec7b2eeba4f9eb69a7b2cb11bfeb4
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
@@ -1,5 +1,7 @@
1
- source "https://rubygems.org"
1
+ # frozen_string_literal: true
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
6
 
5
7
  gemspec
data/README.md CHANGED
@@ -28,7 +28,7 @@ class Home < Radical::Controller
28
28
  end
29
29
 
30
30
  class Routes < Radical::Routes
31
- root 'Home'
31
+ root :Home
32
32
  end
33
33
 
34
34
  class App < Radical::App
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 'routes'
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 ||= route_class
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, path: '/',
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 Rack::Flash, sweep: true
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
@@ -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$/, '').gsub(/([A-Z])/, '_\1')[1..-1].downcase
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
- sig { params(request: Rack::Request).void }
76
- def initialize(request)
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 = '' if @output.nil?
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('_buf', block.binding)
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
@@ -80,7 +80,7 @@ module Radical
80
80
  end
81
81
 
82
82
  def migrations
83
- Dir[File.join(migrations_path || '.', 'db', 'migrations', '*.rb')].sort
83
+ Dir[File.join(migrations_path || '.', 'migrations', '*.rb')].sort
84
84
  end
85
85
 
86
86
  def version(filename)
@@ -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
- @route_name = @controller.class.route_name
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
- @action = if @model.saved?
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 text(name)
22
- "<input type=text name=#{@route_name}[#{name}] value=\"#{@model.public_send(name)}\" />"
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(name)
26
- "<button type=submit>#{name}</button>"
45
+ def button(attrs = {}, &block)
46
+ tag 'button', attrs, &block
27
47
  end
28
48
 
29
- def submit(value)
30
- "<input type=submit value=#{value} />"
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=#{@action} method=#{@method}>"
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
- "<input type=hidden name=_method value=#{@override_method} />" unless %w[GET POST].include?(@override_method)
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,5 @@
1
+ <<-RB
2
+ RADICAL_ENV=development
3
+ SESSION_SECRET=#{SecureRandom.hex(32)}
4
+ DATABASE_URL=development.sqlite3
5
+ RB
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gem 'radical', '~> 1.0'
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'app'
4
+
5
+ run App