jun 0.1.0 → 0.3.1
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/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
|
+
[](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
|