radical 1.0.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +0 -2
  3. data/CHANGELOG.md +46 -1
  4. data/Gemfile +4 -2
  5. data/README.md +5 -1
  6. data/exe/rad +41 -0
  7. data/lib/radical/app.rb +61 -27
  8. data/lib/radical/asset.rb +24 -0
  9. data/lib/radical/asset_compiler.rb +40 -0
  10. data/lib/radical/assets.rb +45 -0
  11. data/lib/radical/controller.rb +54 -11
  12. data/lib/radical/database.rb +43 -44
  13. data/lib/radical/env.rb +1 -0
  14. data/lib/radical/flash.rb +61 -0
  15. data/lib/radical/form.rb +75 -15
  16. data/lib/radical/generator/app/.env +5 -0
  17. data/lib/radical/generator/app/Gemfile +7 -0
  18. data/lib/radical/generator/app/app.rb +37 -0
  19. data/lib/radical/generator/app/config.ru +5 -0
  20. data/lib/radical/generator/app/controllers/controller.rb +4 -0
  21. data/lib/radical/generator/app/models/model.rb +4 -0
  22. data/lib/radical/generator/app/routes.rb +5 -0
  23. data/lib/radical/generator/blank_migration.rb +11 -0
  24. data/lib/radical/generator/controller.rb +59 -0
  25. data/lib/radical/generator/migration.rb +13 -0
  26. data/lib/radical/generator/model.rb +9 -0
  27. data/lib/radical/generator/views/_form.rb +6 -0
  28. data/lib/radical/generator/views/edit.rb +3 -0
  29. data/lib/radical/generator/views/index.rb +24 -0
  30. data/lib/radical/generator/views/new.rb +4 -0
  31. data/lib/radical/generator/views/show.rb +5 -0
  32. data/lib/radical/generator.rb +155 -0
  33. data/lib/radical/migration.rb +45 -0
  34. data/lib/radical/model.rb +3 -12
  35. data/lib/radical/router.rb +143 -42
  36. data/lib/radical/routes.rb +59 -0
  37. data/lib/radical/security_headers.rb +27 -0
  38. data/lib/radical/strings.rb +17 -0
  39. data/lib/radical/table.rb +2 -0
  40. data/lib/radical/view.rb +19 -9
  41. data/lib/radical.rb +11 -0
  42. data/radical.gemspec +4 -3
  43. metadata +44 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 537aa5f33439e08833e0aca3ff572a0e017c20cdbfaa3d267c303191cc692fd0
4
- data.tar.gz: 95ee9c628803bf81f237d15aa6c3d92a1d0c55f4412809eb4e74a57dd510e15c
3
+ metadata.gz: 3d14c9a576701105d60a08c6339a19aeebc080b6a4fd6f7235d8c5dec82ea287
4
+ data.tar.gz: b03f1d945e0f441401d99375062423dff70fa9c50cc394e470444d253faa46c4
5
5
  SHA512:
6
- metadata.gz: f30a37e86df78f5beb7a345b96031a1d83779a723629a7b1ca3c70223d7cea94acfc277bea77d32b8461382fbed2f146b3bf69a732a77224fd4d586f7877811d
7
- data.tar.gz: a322aef87deae2b114842c4a88ac31fc02f35258701770d9a2017158616a6d07229a5960de8879efbd403b286d4c1a53ec64ba7f9b7e202570734f8b9de0c5b1
6
+ metadata.gz: cbdbdd7c8a1a56755ef772bbd7ce24a197111d97d44be0d1c1f6a56be24361d1f80469583f4969c0d8b8d9500ce92a66333e71539e8a79b845f249659b72a6ec
7
+ data.tar.gz: d3425aef0d2a3d87788b7169ba81b67b8c71faed045227aaff586a699ad46482d0770cfaacf0a738d787bb28b20be42c47b7d88ef79eda5632af5abe91da1e1b
data/.rubocop.yml CHANGED
@@ -1,4 +1,2 @@
1
- Style/FrozenStringLiteralComment:
2
- Enabled: false
3
1
  Style/Documentation:
4
2
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,9 +1,50 @@
1
1
  # Changelog
2
+
2
3
  All notable changes to this project will be documented in this file
3
4
 
4
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5
6
 
6
- ## [Unreleased]
7
+ # [Unreleased]
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
+
29
+ # 1.1.0 (2021-12-06)
30
+
31
+ - *Breaking* `root`, `resource` and `resources` methods no longer take class constants
32
+ - *Breaking* Move route class methods to `Routes` class instead of `App`
33
+ - *Breaking* Create migration class, use migration classes in migrate!
34
+ - *Breaking* Make routes take symbols or strings, not classes to better line up with models
35
+ - *Breaking* Move connection string handling to database
36
+ - Make everything use `frozen_string_literal`
37
+ - Purposefully never add callbacks, `before_action` or autoloading
38
+
39
+ # 1.0.2 (2021-12-02)
40
+
41
+ - Set default views / migrations paths
42
+
43
+ # 1.0.1 (2021-12-02)
44
+
45
+ - Fix changelog link in gemspec
46
+
47
+ # 1.0.0 (2021-12-01)
7
48
 
8
49
  - Very basic understanding of how much memory it takes for a basic ruby app
9
50
  - Start to integrate controllers and routing
@@ -25,3 +66,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
25
66
  - Use Rack::Test
26
67
  - Rename routes to resources; add resource method
27
68
  - Add nested resources, multi-argument resources
69
+ - Use rack sessions, rack-csrf and rack-flash3
70
+ - Add naive migrations
71
+ - Add naive models
72
+ - Add naive form helper
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
@@ -27,8 +27,12 @@ class Home < Radical::Controller
27
27
  end
28
28
  end
29
29
 
30
+ class Routes < Radical::Routes
31
+ root :Home
32
+ end
33
+
30
34
  class App < Radical::App
31
- root Home
35
+ routes Routes
32
36
  end
33
37
 
34
38
  run 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,9 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
1
4
  require 'rack'
2
- require 'rack/flash'
3
5
  require 'rack/csrf'
4
6
 
5
- require_relative 'router'
7
+ require_relative 'asset'
8
+ require_relative 'assets'
9
+ require_relative 'asset_compiler'
6
10
  require_relative 'env'
11
+ require_relative 'flash'
12
+ require_relative 'routes'
13
+ require_relative 'security_headers'
7
14
 
8
15
  # The main entry point for a Radical application
9
16
  #
@@ -31,24 +38,39 @@ require_relative 'env'
31
38
  module Radical
32
39
  class App
33
40
  class << self
34
- def root(klass)
35
- router.add_root(klass)
41
+ def routes(route_class)
42
+ @routes = route_class
36
43
  end
37
44
 
38
- def resource(klass)
39
- router.add_actions(klass, actions: Router::RESOURCE_ACTIONS)
45
+ def assets(&block)
46
+ @assets = Assets.new
47
+
48
+ block.call(@assets)
40
49
  end
41
50
 
42
- def resources(*classes, &block)
43
- prefix = "#{router.route_prefix(@parents)}/" if instance_variable_defined?(:@parents)
51
+ def compile_assets
52
+ @assets.compile
53
+ end
44
54
 
45
- router.add_routes(classes, prefix: prefix)
55
+ def serve_assets
56
+ @serve_assets = true
57
+ end
46
58
 
47
- return unless block
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
+ }
48
72
 
49
- @parents ||= []
50
- @parents << classes.last
51
- block.call
73
+ @session = defaults.merge(options)
52
74
  end
53
75
 
54
76
  def env
@@ -56,8 +78,12 @@ module Radical
56
78
  end
57
79
 
58
80
  def app
59
- router = self.router
81
+ router = @routes.router
60
82
  env = self.env
83
+ assets = @assets
84
+ serve_assets = @serve_assets
85
+ security_headers = @security_headers || {}
86
+ session = @session || self.session
61
87
 
62
88
  @app ||= Rack::Builder.app do
63
89
  use Rack::CommonLogger
@@ -65,23 +91,29 @@ module Radical
65
91
  use Rack::Runtime
66
92
  use Rack::MethodOverride
67
93
  use Rack::ContentLength
68
- use Rack::Deflater
69
94
  use Rack::ETag
95
+ use Rack::Deflater
70
96
  use Rack::Head
71
97
  use Rack::ConditionalGet
72
98
  use Rack::ContentType
73
- use Rack::Session::Cookie, path: '/',
74
- secret: ENV['SESSION_SECRET'],
75
- http_only: true,
76
- same_site: :lax,
77
- secure: env.production?,
78
- expire_after: 2_592_000 # 30 days
99
+ use Rack::Session::Cookie, session
79
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)
80
- 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
81
113
 
82
114
  run lambda { |rack_env|
83
115
  begin
84
- router.route(Rack::Request.new(rack_env)).finish
116
+ router.route(Rack::Request.new(rack_env), options: { assets: assets }).finish
85
117
  rescue ModelNotFound
86
118
  raise unless env.production?
87
119
 
@@ -91,13 +123,15 @@ module Radical
91
123
  end
92
124
  end
93
125
 
94
- def router
95
- @router ||= Router.new
96
- end
97
-
98
126
  def call(env)
99
127
  app.call(env)
100
128
  end
129
+
130
+ private
131
+
132
+ def session_secret
133
+ @session_secret ||= (ENV['SESSION_SECRET'] || SecureRandom.hex(32))
134
+ end
101
135
  end
102
136
  end
103
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
@@ -1,4 +1,6 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
3
+
2
4
  require 'rack/utils'
3
5
  require 'rack/request'
4
6
  require 'rack/response'
@@ -29,7 +31,7 @@ module Radical
29
31
 
30
32
  sig { returns(String) }
31
33
  def route_name
32
- to_s.split('::').last.gsub(/Controller$/, '').gsub(/([A-Z])/, '_\1')[1..-1].downcase
34
+ Strings.snake_case to_s.split('::').last.gsub(/Controller$/, '')
33
35
  end
34
36
 
35
37
  sig { params(actions: Symbol).void }
@@ -70,9 +72,12 @@ module Radical
70
72
  end
71
73
  end
72
74
 
73
- sig { params(request: Rack::Request).void }
74
- def initialize(request)
75
+ attr_reader :options
76
+
77
+ sig { params(request: Rack::Request, options: T.nilable(Hash)).void }
78
+ def initialize(request, options: {})
75
79
  @request = request
80
+ @options = options
76
81
  end
77
82
 
78
83
  sig { params(status: T.any(Symbol, Integer)).returns(Rack::Response) }
@@ -90,14 +95,14 @@ module Radical
90
95
  @request.params
91
96
  end
92
97
 
93
- sig { params(name: T.any(String, Symbol)).returns(String) }
94
- def view(name)
95
- 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 })
96
101
  end
97
102
 
98
- sig { params(name: T.any(String, Symbol)).returns(String) }
99
- def partial(name)
100
- 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 })
101
106
  end
102
107
 
103
108
  sig { params(options: Hash, block: T.proc.void).returns(String) }
@@ -128,17 +133,55 @@ module Radical
128
133
  @request.env['rack.session']
129
134
  end
130
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
+
131
166
  private
132
167
 
133
168
  def emit(tag)
134
- @output = '' if @output.nil?
169
+ @output = String.new if @output.nil?
135
170
  @output << tag.to_s
136
171
  end
137
172
 
138
173
  def capture(block)
139
- @output = eval('_buf', block.binding)
174
+ @output = eval '_buf', block.binding
140
175
  yield
141
176
  @output
142
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
143
186
  end
144
187
  end
@@ -1,40 +1,67 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sqlite3'
2
4
  require_relative 'table'
5
+ require_relative 'migration'
3
6
 
4
7
  module Radical
5
8
  class Database
6
9
  class << self
7
- attr_accessor :connection, :migrations_path
10
+ attr_writer :connection_string
11
+ attr_accessor :migrations_path
12
+
13
+ def connection_string
14
+ @connection_string || ENV['DATABASE_URL']
15
+ end
16
+
17
+ def connection
18
+ conn = SQLite3::Database.new(connection_string)
19
+ conn.results_as_hash = true
20
+ conn.type_translation = true
21
+
22
+ @connection ||= conn
23
+ end
24
+
25
+ def prepend_migrations_path(path)
26
+ self.migrations_path = path
27
+ end
8
28
 
9
29
  def db
10
30
  connection
11
31
  end
12
32
 
13
- def migrate!
14
- @migrate = true
33
+ def migration(file)
34
+ context = Module.new
35
+ context.class_eval(File.read(file), file)
36
+ const = context.constants.find do |constant|
37
+ context.const_get(constant).ancestors.include?(Radical::Migration)
38
+ end
39
+
40
+ context.const_get(const)
41
+ end
15
42
 
43
+ def migrate!
16
44
  db.execute 'create table if not exists radical_migrations ( version integer primary key )'
17
45
 
18
- pending_migrations.each do |migration|
19
- puts "Executing migration #{migration}"
20
- sql = eval File.read(migration)
21
- db.execute sql
22
- db.execute 'insert into radical_migrations (version) values (?)', [version(migration)]
46
+ pending_migrations.each do |file|
47
+ puts "Executing migration #{file}"
48
+
49
+ v = version(file)
50
+
51
+ migration(file).migrate!(db: db, version: v)
23
52
  end
24
53
  end
25
54
 
26
55
  def rollback!
27
- @rollback = true
28
-
29
56
  db.execute 'create table if not exists radical_migrations ( version integer primary key )'
30
57
 
31
- migration = applied_migrations.last
58
+ file = applied_migrations.last
32
59
 
33
- puts "Rolling back migration #{migration}"
60
+ puts "Rolling back migration #{file}"
34
61
 
35
- sql = eval File.read(migration)
36
- db.execute sql
37
- db.execute 'delete from radical_migrations where version = ?', [version(migration)]
62
+ v = version(file)
63
+
64
+ migration(file).rollback!(db: db, version: v)
38
65
  end
39
66
 
40
67
  def applied_versions
@@ -53,40 +80,12 @@ module Radical
53
80
  end
54
81
 
55
82
  def migrations
56
- Dir[File.join(migrations_path, 'db', 'migrations', '*.rb')].sort
83
+ Dir[File.join(migrations_path || '.', 'migrations', '*.rb')].sort
57
84
  end
58
85
 
59
86
  def version(filename)
60
87
  filename.split(File::SEPARATOR).last.split('_').first.to_i
61
88
  end
62
-
63
- def migration(&block)
64
- block.call
65
- end
66
-
67
- def change(&block)
68
- @change = true
69
-
70
- block.call
71
- end
72
-
73
- def up(&block)
74
- block.call
75
- end
76
-
77
- def down(&block)
78
- block.call
79
- end
80
-
81
- def create_table(name, &block)
82
- return "drop table #{name}" if @change && @rollback
83
-
84
- table = Table.new(name)
85
-
86
- block.call(table)
87
-
88
- "create table #{name} ( id integer primary key, #{table.columns.join(',')} )"
89
- end
90
89
  end
91
90
  end
92
91
  end
data/lib/radical/env.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # typed: true
2
3
 
3
4
  module Radical