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