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