jun 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -15
  3. data/jun.gemspec +4 -4
  4. data/lib/jun/action_controller/base.rb +6 -10
  5. data/lib/jun/action_controller/callbacks.rb +46 -0
  6. data/lib/jun/action_controller/metal.rb +22 -0
  7. data/lib/jun/action_controller/redirecting.rb +17 -0
  8. data/lib/jun/action_controller/rendering.rb +0 -4
  9. data/lib/jun/action_dispatch/routing/mapper.rb +28 -3
  10. data/lib/jun/action_dispatch/routing/route_set.rb +49 -2
  11. data/lib/jun/action_dispatch/routing/welcome.html.erb +59 -0
  12. data/lib/jun/{active_record.rb → active_record/base.rb} +14 -3
  13. data/lib/jun/active_record/migration.rb +76 -0
  14. data/lib/jun/active_record/migrator.rb +80 -0
  15. data/lib/jun/active_record/persistence.rb +60 -0
  16. data/lib/jun/active_record/relation.rb +42 -0
  17. data/lib/jun/active_support/core_ext/array/access.rb +33 -0
  18. data/lib/jun/active_support/core_ext/array/conversion.rb +18 -0
  19. data/lib/jun/active_support/core_ext/hash/transformation.rb +29 -0
  20. data/lib/jun/active_support/core_ext/string/access.rb +42 -0
  21. data/lib/jun/active_support/{inflector.rb → core_ext/string/inflector.rb} +18 -0
  22. data/lib/jun/active_support/core_ext.rb +5 -0
  23. data/lib/jun/active_support/dependencies.rb +2 -0
  24. data/lib/jun/application.rb +31 -0
  25. data/lib/jun/cli/commands/db/create.rb +21 -1
  26. data/lib/jun/cli/commands/db/drop.rb +4 -1
  27. data/lib/jun/cli/commands/db/migrate.rb +1 -1
  28. data/lib/jun/cli/commands/db/rollback.rb +15 -0
  29. data/lib/jun/cli/commands/db/schema/dump.rb +24 -0
  30. data/lib/jun/cli/commands/db/schema/load.rb +43 -0
  31. data/lib/jun/cli/commands/db/seed.rb +19 -0
  32. data/lib/jun/cli/commands/generate/migration.rb +27 -0
  33. data/lib/jun/cli/commands/new.rb +7 -2
  34. data/lib/jun/cli/commands/server.rb +1 -1
  35. data/lib/jun/cli/generator_templates/migration.rb.erb +11 -0
  36. data/lib/jun/cli/{generators/new → generator_templates/new_app}/Gemfile.erb +0 -0
  37. data/lib/jun/cli/{generators/new → generator_templates/new_app}/README.md.erb +0 -0
  38. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/controllers/application_controller.rb.erb +0 -0
  39. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/helpers/application_helper.rb.erb +0 -0
  40. data/lib/jun/cli/generator_templates/new_app/app/models/application_record.rb.erb +4 -0
  41. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/views/layouts/application.html.erb.erb +0 -0
  42. data/lib/jun/cli/generator_templates/new_app/bin/console.erb +8 -0
  43. data/lib/jun/cli/generator_templates/new_app/config/application.rb.erb +12 -0
  44. data/lib/jun/cli/generator_templates/new_app/config/environment.rb.erb +7 -0
  45. data/lib/jun/cli/{generators/new → generator_templates/new_app}/config/routes.rb.erb +0 -0
  46. data/lib/jun/cli/{generators/new → generator_templates/new_app}/config.ru.erb +1 -2
  47. data/lib/jun/cli/generator_templates/new_app/db/seeds.rb.erb +9 -0
  48. data/lib/jun/cli.rb +5 -0
  49. data/lib/jun/version.rb +1 -1
  50. data/lib/jun.rb +13 -2
  51. metadata +48 -26
  52. data/lib/jun/cli/generators/new/config/application.rb.erb +0 -18
  53. 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: a23824148b7546d35ec51bc05200095f47bb5ffcb3604597a2c1606b893ae1c5
4
- data.tar.gz: e6e03e5e69d3cebd025a9194de2488539a096c9864fbb53b720f43c5f7d5f68b
3
+ metadata.gz: febb60185e9d61a52eb783ec6052f39a35b7a41afca6bf27cf8db4bea718536f
4
+ data.tar.gz: 8c4068f3c9093bc6b9184d1e9eeda93e06ff4ba11bf826c4dec93feb218efc73
5
5
  SHA512:
6
- metadata.gz: 0a50ed3137ed9926647d32e534c908a1f6bca9e235a6de53f6969932be97dca05bbb44dec9bffd4d3de3b1b5d96cdcc227b2c0728a176a3433dd98cb85e0b403
7
- data.tar.gz: 78931eee706054b9b6f85585b3dd0af3055a9ff6641439802aea254c1f3c0c23292802e4c40147f17bea8e72b6d6e1ce5fe3b85455260ef559ca9dedeadcc40e
6
+ metadata.gz: '0957dea84721aec8cbb82eeef95d7516c5862960683086686ee6b1ee2e0fae69ff4dbf72d145530d174f9220cd8e384206160a1264a9eba5eeaf334d51bd8674'
7
+ data.tar.gz: 5e90b858218ba802b04d5d7752f227947493b0c65243437faf8757147c726815794dd6f921521ce438b03ffc66883152379988b4b05058ad70276879c6d26e0d
data/README.md CHANGED
@@ -1,32 +1,35 @@
1
1
  # Jun
2
2
 
3
- Jun is a simple, Rails-inspired web application framework. It is built with the goal of learning more about Rails internals. Not meant for production use.
3
+ [![Gem Version](https://badge.fury.io/rb/jun.svg)](https://badge.fury.io/rb/jun)
4
4
 
5
- ## Installation
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
- Add this line to your application's Gemfile:
7
+ ## Getting Started
8
8
 
9
- ```ruby
10
- gem 'jun'
11
- ```
9
+ Install the gem:
12
10
 
13
- And then execute:
11
+ ```
12
+ $ gem install jun
13
+ ```
14
14
 
15
- $ bundle
15
+ Then, create a new Jun application:
16
16
 
17
- Or install it yourself as:
17
+ ```
18
+ $ jun new my_app
19
+ ```
18
20
 
19
- $ gem install jun
21
+ Change directory into `my_app` and start up the server:
20
22
 
21
- ## Usage
23
+ ```
24
+ $ cd my_app
25
+ $ bin/jun server
26
+ ```
22
27
 
23
- _TBD_
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 also run `bin/console` for an interactive prompt that will allow you to experiment.
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
@@ -71,10 +71,6 @@ module Jun
71
71
 
72
72
  private
73
73
 
74
- def response_rendered?
75
- !!@_response_rendered
76
- end
77
-
78
74
  def views_path
79
75
  dirname = self.class.name.sub(/Controller\z/, "").underscore
80
76
  Jun.root.join("app/views/#{dirname}")
@@ -9,10 +9,19 @@ module Jun
9
9
  end
10
10
 
11
11
  def get(path, to:, as: nil)
12
- controller, action = to.split("#")
13
- path = path.to_s.start_with?("/") ? path.to_s : "/#{path}"
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
- @route_set.add_route("GET", path, controller, action, as)
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
- [404, { "Content-Type" => "text/plain" }, ["Not found"]]
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
- find_by_sql("SELECT * FROM #{table_name}")
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