jun 0.1.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +18 -15
- data/jun.gemspec +4 -4
- data/lib/jun/action_controller/base.rb +6 -10
- data/lib/jun/action_controller/callbacks.rb +46 -0
- data/lib/jun/action_controller/metal.rb +22 -0
- data/lib/jun/action_controller/redirecting.rb +17 -0
- data/lib/jun/action_controller/rendering.rb +0 -4
- data/lib/jun/action_dispatch/routing/mapper.rb +28 -3
- data/lib/jun/action_dispatch/routing/route_set.rb +49 -2
- data/lib/jun/action_dispatch/routing/welcome.html.erb +59 -0
- data/lib/jun/{active_record.rb → active_record/base.rb} +14 -3
- data/lib/jun/active_record/migration.rb +76 -0
- data/lib/jun/active_record/migrator.rb +80 -0
- data/lib/jun/active_record/persistence.rb +60 -0
- data/lib/jun/active_record/relation.rb +42 -0
- data/lib/jun/active_support/core_ext/array/access.rb +33 -0
- data/lib/jun/active_support/core_ext/array/conversion.rb +18 -0
- data/lib/jun/active_support/core_ext/hash/transformation.rb +29 -0
- data/lib/jun/active_support/core_ext/string/access.rb +42 -0
- data/lib/jun/active_support/{inflector.rb → core_ext/string/inflector.rb} +18 -0
- data/lib/jun/active_support/core_ext.rb +5 -0
- data/lib/jun/active_support/dependencies.rb +2 -0
- data/lib/jun/application.rb +31 -0
- data/lib/jun/cli/commands/db/create.rb +21 -1
- data/lib/jun/cli/commands/db/drop.rb +4 -1
- data/lib/jun/cli/commands/db/migrate.rb +1 -1
- data/lib/jun/cli/commands/db/rollback.rb +15 -0
- data/lib/jun/cli/commands/db/schema/dump.rb +24 -0
- data/lib/jun/cli/commands/db/schema/load.rb +43 -0
- data/lib/jun/cli/commands/db/seed.rb +19 -0
- data/lib/jun/cli/commands/generate/migration.rb +27 -0
- data/lib/jun/cli/commands/new.rb +7 -2
- data/lib/jun/cli/commands/server.rb +1 -1
- data/lib/jun/cli/generator_templates/migration.rb.erb +11 -0
- data/lib/jun/cli/{generators/new → generator_templates/new_app}/Gemfile.erb +0 -0
- data/lib/jun/cli/{generators/new → generator_templates/new_app}/README.md.erb +0 -0
- data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/controllers/application_controller.rb.erb +0 -0
- data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/helpers/application_helper.rb.erb +0 -0
- data/lib/jun/cli/generator_templates/new_app/app/models/application_record.rb.erb +4 -0
- data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/views/layouts/application.html.erb.erb +0 -0
- data/lib/jun/cli/generator_templates/new_app/bin/console.erb +8 -0
- data/lib/jun/cli/generator_templates/new_app/config/application.rb.erb +12 -0
- data/lib/jun/cli/generator_templates/new_app/config/environment.rb.erb +7 -0
- data/lib/jun/cli/{generators/new → generator_templates/new_app}/config/routes.rb.erb +0 -0
- data/lib/jun/cli/{generators/new → generator_templates/new_app}/config.ru.erb +1 -2
- data/lib/jun/cli/generator_templates/new_app/db/seeds.rb.erb +9 -0
- data/lib/jun/cli.rb +5 -0
- data/lib/jun/version.rb +1 -1
- data/lib/jun.rb +13 -2
- metadata +48 -26
- data/lib/jun/cli/generators/new/config/application.rb.erb +0 -18
- data/lib/jun/cli/generators/new/db/app.db.erb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: febb60185e9d61a52eb783ec6052f39a35b7a41afca6bf27cf8db4bea718536f
|
4
|
+
data.tar.gz: 8c4068f3c9093bc6b9184d1e9eeda93e06ff4ba11bf826c4dec93feb218efc73
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0957dea84721aec8cbb82eeef95d7516c5862960683086686ee6b1ee2e0fae69ff4dbf72d145530d174f9220cd8e384206160a1264a9eba5eeaf334d51bd8674'
|
7
|
+
data.tar.gz: 5e90b858218ba802b04d5d7752f227947493b0c65243437faf8757147c726815794dd6f921521ce438b03ffc66883152379988b4b05058ad70276879c6d26e0d
|
data/README.md
CHANGED
@@ -1,32 +1,35 @@
|
|
1
1
|
# Jun
|
2
2
|
|
3
|
-
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/jun.svg)](https://badge.fury.io/rb/jun)
|
4
4
|
|
5
|
-
|
5
|
+
Jun is a simple, [Rails](https://github.com/rails/rails)-inspired web application framework. This is a rough implementation, built with the goal of learning more about Rails internals. Not meant for production use.
|
6
6
|
|
7
|
-
|
7
|
+
## Getting Started
|
8
8
|
|
9
|
-
|
10
|
-
gem 'jun'
|
11
|
-
```
|
9
|
+
Install the gem:
|
12
10
|
|
13
|
-
|
11
|
+
```
|
12
|
+
$ gem install jun
|
13
|
+
```
|
14
14
|
|
15
|
-
|
15
|
+
Then, create a new Jun application:
|
16
16
|
|
17
|
-
|
17
|
+
```
|
18
|
+
$ jun new my_app
|
19
|
+
```
|
18
20
|
|
19
|
-
|
21
|
+
Change directory into `my_app` and start up the server:
|
20
22
|
|
21
|
-
|
23
|
+
```
|
24
|
+
$ cd my_app
|
25
|
+
$ bin/jun server
|
26
|
+
```
|
22
27
|
|
23
|
-
|
28
|
+
Visit `http://localhost:6291` to view your app.
|
24
29
|
|
25
30
|
## Development
|
26
31
|
|
27
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
|
28
|
-
|
29
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
33
|
|
31
34
|
## License
|
32
35
|
|
data/jun.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.email = ["zspesic@gmail.com"]
|
10
10
|
|
11
11
|
spec.summary = "A simple Ruby web framework."
|
12
|
-
spec.description = "A simple web framework inspired by Rails."
|
12
|
+
spec.description = "A simple web framework inspired by Rails. Not meant for production use."
|
13
13
|
spec.homepage = "https://github.com/zokioki/jun"
|
14
14
|
spec.license = "MIT"
|
15
15
|
spec.required_ruby_version = ">= 2.6.0"
|
@@ -24,9 +24,9 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
25
25
|
spec.require_paths = ["lib"]
|
26
26
|
|
27
|
-
spec.add_runtime_dependency "rack"
|
28
|
-
spec.add_runtime_dependency "tilt"
|
29
|
-
spec.add_runtime_dependency "sqlite3"
|
27
|
+
spec.add_runtime_dependency "rack", "~> 2.2"
|
28
|
+
spec.add_runtime_dependency "tilt", "~> 2.0"
|
29
|
+
spec.add_runtime_dependency "sqlite3", "~> 1.4"
|
30
30
|
|
31
31
|
spec.add_development_dependency "rake", "~> 13.0"
|
32
32
|
spec.add_development_dependency "minitest", "~> 5.0"
|
@@ -1,20 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "metal"
|
4
|
+
require_relative "callbacks"
|
3
5
|
require_relative "rendering"
|
6
|
+
require_relative "redirecting"
|
4
7
|
|
5
8
|
module Jun
|
6
9
|
module ActionController
|
7
|
-
class Base
|
10
|
+
class Base < Metal
|
11
|
+
include Jun::ActionController::Callbacks
|
8
12
|
include Jun::ActionController::Rendering
|
9
|
-
|
10
|
-
attr_accessor :request, :response
|
11
|
-
|
12
|
-
def handle_response(action)
|
13
|
-
public_send(action)
|
14
|
-
render(action) unless response_rendered?
|
15
|
-
|
16
|
-
response
|
17
|
-
end
|
13
|
+
include Jun::ActionController::Redirecting
|
18
14
|
end
|
19
15
|
end
|
20
16
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jun
|
4
|
+
module ActionController
|
5
|
+
module Callbacks
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
def handle_response(action)
|
11
|
+
self.class.before_actions.each do |callback|
|
12
|
+
callback.call(self) if callback.match?(action)
|
13
|
+
end
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def before_action(method_name, options = {})
|
20
|
+
before_actions << Callback.new(method_name, options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def before_actions
|
24
|
+
@before_actions ||= []
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Callback
|
29
|
+
def initialize(method_name, options)
|
30
|
+
@method_name = method_name
|
31
|
+
@options = options
|
32
|
+
end
|
33
|
+
|
34
|
+
def match?(action)
|
35
|
+
return true unless @options[:only]&.any?
|
36
|
+
|
37
|
+
@options[:only].include?(action.to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(controller)
|
41
|
+
controller.send(@method_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jun
|
4
|
+
module ActionController
|
5
|
+
class Metal
|
6
|
+
attr_accessor :request, :response
|
7
|
+
|
8
|
+
def handle_response(action)
|
9
|
+
public_send(action)
|
10
|
+
render(action) unless response_rendered?
|
11
|
+
|
12
|
+
response
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def response_rendered?
|
18
|
+
!!@_response_rendered
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jun
|
4
|
+
module ActionController
|
5
|
+
module Redirecting
|
6
|
+
def redirect_to(location, options = {})
|
7
|
+
return if response_rendered?
|
8
|
+
|
9
|
+
response.location = location
|
10
|
+
response.status = options[:status] || 302
|
11
|
+
response.write("<html><body>You are being <a href=\"#{response.location}\">redirected</a>.</body></html>")
|
12
|
+
|
13
|
+
@_response_rendered = true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -9,10 +9,19 @@ module Jun
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def get(path, to:, as: nil)
|
12
|
-
|
13
|
-
|
12
|
+
add_route(:get, path, to: to, as: as)
|
13
|
+
end
|
14
|
+
|
15
|
+
def post(path, to:, as: nil)
|
16
|
+
add_route(:post, path, to: to, as: as)
|
17
|
+
end
|
18
|
+
|
19
|
+
def patch(path, to:, as: nil)
|
20
|
+
add_route(:patch, path, to: to, as: as)
|
21
|
+
end
|
14
22
|
|
15
|
-
|
23
|
+
def delete(path, to:, as: nil)
|
24
|
+
add_route(:delete, path, to: to, as: as)
|
16
25
|
end
|
17
26
|
|
18
27
|
def root(to:)
|
@@ -22,6 +31,22 @@ module Jun
|
|
22
31
|
def resources(plural_name)
|
23
32
|
get "/#{plural_name}", to: "#{plural_name}#index", as: plural_name.to_s
|
24
33
|
get "/#{plural_name}/new", to: "#{plural_name}#new", as: "new_#{plural_name.to_s.singularize}"
|
34
|
+
post "/#{plural_name}", to: "#{plural_name}#create"
|
35
|
+
get "/#{plural_name}/:id", to: "#{plural_name}#show", as: plural_name.to_s.singularize
|
36
|
+
get "/#{plural_name}/:id/edit", to: "#{plural_name}#edit", as: "edit_#{plural_name.to_s.singularize}"
|
37
|
+
patch "/#{plural_name}/:id", to: "#{plural_name}#update"
|
38
|
+
delete "/#{plural_name}/:id", to: "#{plural_name}#destroy"
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def add_route(method, path, to:, as: nil)
|
44
|
+
method = method.to_s.upcase
|
45
|
+
controller, action = to.split("#")
|
46
|
+
path = path.to_s.start_with?("/") ? path.to_s : "/#{path}"
|
47
|
+
as ||= path.sub("/", "")
|
48
|
+
|
49
|
+
@route_set.add_route(method, path, controller, action, as)
|
25
50
|
end
|
26
51
|
end
|
27
52
|
end
|
@@ -25,7 +25,7 @@ module Jun
|
|
25
25
|
|
26
26
|
def path_regex
|
27
27
|
path_string_for_regex = path.gsub(/:\w+/) { |match| "(?<#{match.delete(":")}>\\w+)" }
|
28
|
-
Regexp.new("^#{path_string_for_regex}
|
28
|
+
Regexp.new("^#{path_string_for_regex}\/?$")
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -35,18 +35,21 @@ module Jun
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def call(env)
|
38
|
+
return welcome_response if @routes.none?
|
39
|
+
|
38
40
|
request = Rack::Request.new(env)
|
39
41
|
|
40
42
|
if route = find_route(request)
|
41
43
|
route.dispatch(request)
|
42
44
|
else
|
43
|
-
|
45
|
+
not_found_response
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
47
49
|
def add_route(*args)
|
48
50
|
route = Route.new(*args)
|
49
51
|
@routes.push(route)
|
52
|
+
define_url_helper(route)
|
50
53
|
|
51
54
|
route
|
52
55
|
end
|
@@ -59,6 +62,50 @@ module Jun
|
|
59
62
|
mapper = Jun::ActionDispatch::Routing::Mapper.new(self)
|
60
63
|
mapper.instance_eval(&block)
|
61
64
|
end
|
65
|
+
|
66
|
+
def url_helpers
|
67
|
+
@url_helpers ||= Module.new.extend(url_helpers_module).include(url_helpers_module)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def url_helpers_module
|
73
|
+
@url_helpers_module ||= Module.new
|
74
|
+
end
|
75
|
+
|
76
|
+
def define_url_helper(route)
|
77
|
+
path_name = "#{route.name}_path"
|
78
|
+
|
79
|
+
url_helpers_module.define_method(path_name) do |*args|
|
80
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
81
|
+
path = route.path
|
82
|
+
path_tokens = path.scan(/:\w+/)
|
83
|
+
|
84
|
+
path_tokens.each.with_index do |token, index|
|
85
|
+
path.sub!(token, args[index])
|
86
|
+
end
|
87
|
+
|
88
|
+
path
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def not_found_response
|
93
|
+
response = Rack::Response.new
|
94
|
+
response.content_type = "text/plain"
|
95
|
+
response.status = 404
|
96
|
+
response.write("Not found")
|
97
|
+
response.finish
|
98
|
+
end
|
99
|
+
|
100
|
+
def welcome_response
|
101
|
+
template_filepath = File.expand_path("welcome.html.erb", __dir__)
|
102
|
+
template = Tilt::ERBTemplate.new(template_filepath)
|
103
|
+
|
104
|
+
response = Rack::Response.new
|
105
|
+
response.content_type = "text/html"
|
106
|
+
response.write(template.render)
|
107
|
+
response.finish
|
108
|
+
end
|
62
109
|
end
|
63
110
|
end
|
64
111
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<title>Welcome to Jun!</title>
|
5
|
+
<meta charset="utf-8">
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
7
|
+
|
8
|
+
<style>
|
9
|
+
body {
|
10
|
+
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
|
11
|
+
background-color: #fff3f3;
|
12
|
+
color: #7d3737;
|
13
|
+
}
|
14
|
+
|
15
|
+
::selection {
|
16
|
+
background-color: #ffbdb6;
|
17
|
+
}
|
18
|
+
|
19
|
+
.root {
|
20
|
+
margin-top: 4rem;
|
21
|
+
text-align: center;
|
22
|
+
}
|
23
|
+
|
24
|
+
.button {
|
25
|
+
background-color: #fc7e70;
|
26
|
+
color: #fff;
|
27
|
+
margin: 0 0.2rem;
|
28
|
+
padding: 0.5rem 0.75rem;
|
29
|
+
border-radius: 3px;
|
30
|
+
text-decoration: none;
|
31
|
+
}
|
32
|
+
|
33
|
+
.docs-links {
|
34
|
+
margin: 1.5rem 0;
|
35
|
+
}
|
36
|
+
|
37
|
+
.sunspot {
|
38
|
+
width: 100px;
|
39
|
+
height: 100px;
|
40
|
+
border-radius: 50%;
|
41
|
+
background-color: #fc7e70;
|
42
|
+
margin: 2rem auto;
|
43
|
+
}
|
44
|
+
</style>
|
45
|
+
</head>
|
46
|
+
|
47
|
+
<body>
|
48
|
+
<div class="root">
|
49
|
+
<div class="sunspot"></div>
|
50
|
+
<h1>Welcome to Jun!</h1>
|
51
|
+
<p>Get started by checking out the guide or reading up on the docs.</p>
|
52
|
+
<div class="docs-links">
|
53
|
+
<a class="button" href="https://www.rubydoc.info/gems/jun/<%= Jun::VERSION %>#getting-started">Getting Started</a>
|
54
|
+
<a class="button" href="https://www.rubydoc.info/gems/jun/<%= Jun::VERSION %>">Documentation</a>
|
55
|
+
</div>
|
56
|
+
<small>Jun v<%= Jun::VERSION %> | Ruby v<%= RUBY_VERSION %> (<%= RUBY_PLATFORM %>)</small>
|
57
|
+
</div>
|
58
|
+
</body>
|
59
|
+
</html>
|
@@ -1,11 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "connection_adapters/sqlite_adapter"
|
3
|
+
require_relative "../connection_adapters/sqlite_adapter"
|
4
|
+
require_relative "./persistence"
|
4
5
|
|
5
6
|
module ActiveRecord
|
6
7
|
class Base
|
8
|
+
include ActiveRecord::Persistence
|
9
|
+
|
7
10
|
def initialize(attributes = {})
|
8
11
|
@attributes = attributes
|
12
|
+
@new_record = true
|
9
13
|
end
|
10
14
|
|
11
15
|
def method_missing(name, *args)
|
@@ -21,12 +25,19 @@ module ActiveRecord
|
|
21
25
|
end
|
22
26
|
|
23
27
|
def self.all
|
24
|
-
|
28
|
+
ActiveRecord::Relation.new(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.where(*args)
|
32
|
+
all.where(*args)
|
25
33
|
end
|
26
34
|
|
27
35
|
def self.find_by_sql(sql)
|
28
36
|
connection.execute(sql).map do |attributes|
|
29
|
-
new(attributes)
|
37
|
+
object = new(attributes)
|
38
|
+
object.instance_variable_set("@new_record", false)
|
39
|
+
|
40
|
+
object
|
30
41
|
end
|
31
42
|
end
|
32
43
|
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class Migration
|
5
|
+
def up
|
6
|
+
raise NoMethodError, "Subclass must implement method."
|
7
|
+
end
|
8
|
+
|
9
|
+
def down
|
10
|
+
raise NoMethodError, "Subclass must implement method."
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_column(table_name, column_name, column_type, options = {})
|
14
|
+
sql = ["ALTER TABLE #{table_name} ADD COLUMN #{column_name}"]
|
15
|
+
|
16
|
+
sql << column_type.to_s.upcase
|
17
|
+
sql << "NOT NULL" if options[:null] == false
|
18
|
+
sql << "DEFAULT #{options[:default]}" if options[:default]
|
19
|
+
sql << "UNIQUE" if options[:unique] == true
|
20
|
+
|
21
|
+
execute(sql.join(" "))
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_column(table_name, column_name)
|
25
|
+
execute("ALTER TABLE #{table_name} DROP COLUMN #{column_name};")
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_table(table_name, options = {})
|
29
|
+
sql = "CREATE TABLE IF NOT EXISTS #{table_name}"
|
30
|
+
column_options = []
|
31
|
+
|
32
|
+
if options[:id] || !options.key?(:id)
|
33
|
+
column_options << {
|
34
|
+
name: :id,
|
35
|
+
type: :integer,
|
36
|
+
primary_key: true,
|
37
|
+
null: false
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
column_options += options.fetch(:columns, [])
|
42
|
+
|
43
|
+
columns_sql = column_options.map do |column|
|
44
|
+
next(column) if column.is_a?(String)
|
45
|
+
|
46
|
+
column_sql = []
|
47
|
+
|
48
|
+
column_sql << column[:name]
|
49
|
+
column_sql << column[:type]&.to_s&.upcase
|
50
|
+
column_sql << "PRIMARY KEY" if column[:primary_key] == true
|
51
|
+
column_sql << "NOT NULL" if column[:null] == false
|
52
|
+
column_sql << "DEFAULT #{column[:default]}" if column[:default]
|
53
|
+
column_sql << "UNIQUE" if column[:unique] == true
|
54
|
+
|
55
|
+
column_sql.compact.join(" ")
|
56
|
+
end
|
57
|
+
|
58
|
+
sql += " (#{columns_sql.join(", ")})"
|
59
|
+
sql += ";"
|
60
|
+
|
61
|
+
execute(sql)
|
62
|
+
end
|
63
|
+
|
64
|
+
def drop_table(table_name)
|
65
|
+
execute("DROP TABLE IF EXISTS #{table_name};")
|
66
|
+
end
|
67
|
+
|
68
|
+
def rename_table(old_table_name, new_table_name)
|
69
|
+
execute("ALTER TABLE #{old_table_name} RENAME TO #{new_table_name};")
|
70
|
+
end
|
71
|
+
|
72
|
+
def execute(*args)
|
73
|
+
ActiveRecord::Base.connection.execute(*args)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class Migrator
|
5
|
+
ALLOWED_DIRECTIONS = %w[up down].freeze
|
6
|
+
|
7
|
+
def initialize(direction:)
|
8
|
+
unless ALLOWED_DIRECTIONS.include?(direction.to_s)
|
9
|
+
raise ArgumentError, "direction must be one of: #{ALLOWED_DIRECTIONS.inspect}"
|
10
|
+
end
|
11
|
+
|
12
|
+
@direction = direction.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
process_migrations!
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def process_migrations!
|
22
|
+
files_to_process = up_migration? ? pending_migration_files : processed_migration_files.last(1)
|
23
|
+
|
24
|
+
files_to_process.each do |filepath|
|
25
|
+
require filepath
|
26
|
+
|
27
|
+
filename = filepath.split("/").last.sub(".rb", "")
|
28
|
+
migration_version = filename.split("_").first
|
29
|
+
migration_class_name = filename.split("_", 2).last.camelize
|
30
|
+
migration_class = Object.const_get(migration_class_name)
|
31
|
+
|
32
|
+
migration_class.new.public_send(@direction)
|
33
|
+
up_migration? ? add_to_schema_migrations(migration_version) :
|
34
|
+
remove_from_schema_migrations(migration_version)
|
35
|
+
|
36
|
+
migration_verb = up_migration? ? "processed" : "rolled back"
|
37
|
+
puts "Migration #{migration_verb} (#{filename})."
|
38
|
+
end
|
39
|
+
|
40
|
+
dump_schema! if files_to_process.any?
|
41
|
+
end
|
42
|
+
|
43
|
+
def up_migration?
|
44
|
+
@direction == "up"
|
45
|
+
end
|
46
|
+
|
47
|
+
def pending_migration_files
|
48
|
+
@pending_migration_files ||= migration_files - processed_migration_files
|
49
|
+
end
|
50
|
+
|
51
|
+
def processed_migration_files
|
52
|
+
@processed_migration_files ||= migration_files.select do |filepath|
|
53
|
+
file_version = filepath.split("/").last.split("_").first
|
54
|
+
processed_versions.include?(file_version)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def migration_files
|
59
|
+
@migration_files ||= Dir.glob(Jun.root.join("db/migrate/*.rb")).sort
|
60
|
+
end
|
61
|
+
|
62
|
+
def processed_versions
|
63
|
+
ActiveRecord::Base.connection.execute("SELECT * FROM schema_migrations;").map do |attributes|
|
64
|
+
attributes[:version]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_to_schema_migrations(version)
|
69
|
+
ActiveRecord::Base.connection.execute("INSERT INTO schema_migrations (version) VALUES (#{version});")
|
70
|
+
end
|
71
|
+
|
72
|
+
def remove_from_schema_migrations(version)
|
73
|
+
ActiveRecord::Base.connection.execute("DELETE FROM schema_migrations WHERE version = #{version};")
|
74
|
+
end
|
75
|
+
|
76
|
+
def dump_schema!
|
77
|
+
Jun::CLI.process_command(["db:schema:dump"])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Persistence
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def create(attributes = {})
|
11
|
+
object = new(attributes)
|
12
|
+
object.save
|
13
|
+
|
14
|
+
object
|
15
|
+
end
|
16
|
+
|
17
|
+
def primary_key=(value)
|
18
|
+
@primary_key = value.to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def primary_key
|
22
|
+
defined?(@primary_key) ? @primary_key : :id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def save
|
27
|
+
if new_record?
|
28
|
+
result = self.class.connection.execute(
|
29
|
+
<<~SQL
|
30
|
+
INSERT INTO #{self.class.table_name}
|
31
|
+
(#{@attributes.keys.join(",")})
|
32
|
+
VALUES (#{@attributes.values.map { |v| "'#{v}'" }.join(",")})
|
33
|
+
RETURNING *;
|
34
|
+
SQL
|
35
|
+
)
|
36
|
+
|
37
|
+
@attributes[self.class.primary_key] = result.first[self.class.primary_key]
|
38
|
+
@new_record = false
|
39
|
+
else
|
40
|
+
self.class.connection.execute(
|
41
|
+
<<~SQL
|
42
|
+
UPDATE #{self.class.table_name}
|
43
|
+
SET #{@attributes.map { |k, v| "#{k} = #{v.nil? ? 'NULL' : "'#{v}'"}" }.join(",")}
|
44
|
+
WHERE id = #{id};
|
45
|
+
SQL
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def new_record?
|
53
|
+
@new_record
|
54
|
+
end
|
55
|
+
|
56
|
+
def persisted?
|
57
|
+
!new_record?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|