jun 0.1.0 → 0.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -15
  3. data/jun.gemspec +1 -1
  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 +48 -1
  11. data/lib/jun/action_dispatch/routing/welcome.html.erb +32 -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 +46 -0
  16. data/lib/jun/active_record/relation.rb +42 -0
  17. data/lib/jun/active_support/core_ext/array/conversion.rb +10 -0
  18. data/lib/jun/active_support/{inflector.rb → core_ext/string/inflector.rb} +0 -0
  19. data/lib/jun/active_support/core_ext.rb +5 -0
  20. data/lib/jun/application.rb +27 -0
  21. data/lib/jun/cli/commands/db/create.rb +21 -1
  22. data/lib/jun/cli/commands/db/drop.rb +4 -1
  23. data/lib/jun/cli/commands/db/migrate.rb +1 -1
  24. data/lib/jun/cli/commands/db/rollback.rb +15 -0
  25. data/lib/jun/cli/commands/db/schema/dump.rb +24 -0
  26. data/lib/jun/cli/commands/db/schema/load.rb +43 -0
  27. data/lib/jun/cli/commands/db/seed.rb +19 -0
  28. data/lib/jun/cli/commands/generate/migration.rb +27 -0
  29. data/lib/jun/cli/commands/new.rb +7 -2
  30. data/lib/jun/cli/commands/server.rb +1 -1
  31. data/lib/jun/cli/generator_templates/migration.rb.erb +11 -0
  32. data/lib/jun/cli/{generators/new → generator_templates/new_app}/Gemfile.erb +0 -0
  33. data/lib/jun/cli/{generators/new → generator_templates/new_app}/README.md.erb +0 -0
  34. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/controllers/application_controller.rb.erb +0 -0
  35. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/helpers/application_helper.rb.erb +0 -0
  36. data/lib/jun/cli/generator_templates/new_app/app/models/application_record.rb.erb +4 -0
  37. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/views/layouts/application.html.erb.erb +0 -0
  38. data/lib/jun/cli/generator_templates/new_app/bin/console.erb +8 -0
  39. data/lib/jun/cli/generator_templates/new_app/config/application.rb.erb +12 -0
  40. data/lib/jun/cli/generator_templates/new_app/config/environment.rb.erb +7 -0
  41. data/lib/jun/cli/{generators/new → generator_templates/new_app}/config/routes.rb.erb +0 -0
  42. data/lib/jun/cli/{generators/new → generator_templates/new_app}/config.ru.erb +1 -2
  43. data/lib/jun/cli/generator_templates/new_app/db/seeds.rb.erb +9 -0
  44. data/lib/jun/cli.rb +5 -0
  45. data/lib/jun/version.rb +1 -1
  46. data/lib/jun.rb +13 -2
  47. metadata +33 -14
  48. data/lib/jun/cli/generators/new/config/application.rb.erb +0 -18
  49. 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: 0ab6ae7fad2bb74ce92d8ef4a6c0b44c5016b04f02f6721d372694cb3b86c9a5
4
+ data.tar.gz: 5da3c8f0d65029c88022c5b8d37e00e3f92f20ed0cb7592975118eb2608bd11f
5
5
  SHA512:
6
- metadata.gz: 0a50ed3137ed9926647d32e534c908a1f6bca9e235a6de53f6969932be97dca05bbb44dec9bffd4d3de3b1b5d96cdcc227b2c0728a176a3433dd98cb85e0b403
7
- data.tar.gz: 78931eee706054b9b6f85585b3dd0af3055a9ff6641439802aea254c1f3c0c23292802e4c40147f17bea8e72b6d6e1ce5fe3b85455260ef559ca9dedeadcc40e
6
+ metadata.gz: e99d2117bacaf90fd4400bf68725545ce2fd00f9a0292b22ba321cf3af195c1cb6900fcb0f2e66a98277351d7653c13e097270de1b85f8e2050b126e6c5c316b
7
+ data.tar.gz: e81734bc3e01a3682537a928f050c243c2ecb5ee90f15ba10262b872bc8a618d87ba4b4da9d34d108efa2c6d3b33e2230e4a9eed26f685c5d20bb5fcc287a7b8
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"
@@ -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
@@ -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,32 @@
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
+ </style>
24
+ </head>
25
+
26
+ <body>
27
+ <div class="root">
28
+ <h1>Welcome to Jun</h1>
29
+ <small>Jun v<%= Jun::VERSION %> | Ruby v<%= RUBY_VERSION %> (<%= RUBY_PLATFORM %>)</small>
30
+ </div>
31
+ </body>
32
+ </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,46 @@
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("INSERT INTO #{self.class.table_name} (#{@attributes.keys.join(",")}) VALUES (#{@attributes.values.map { |v| "'#{v}'" }.join(",")}) RETURNING *;")
29
+ @attributes[self.class.primary_key] = result.first[self.class.primary_key]
30
+ @new_record = false
31
+ else
32
+ self.class.connection.execute("UPDATE #{self.class.table_name} SET #{@attributes.map { |k, v| "#{k} = #{v}" }.join(",")} WHERE id = #{id};")
33
+ end
34
+
35
+ true
36
+ end
37
+
38
+ def new_record?
39
+ @new_record
40
+ end
41
+
42
+ def persisted?
43
+ !new_record?
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class Relation
5
+ def initialize(klass)
6
+ @klass = klass
7
+ @where_clauses = []
8
+ end
9
+
10
+ def where!(condition)
11
+ clause = condition.is_a?(String) ? condition : condition.map { |k, v| "#{k} = #{v}" }.join(" AND ")
12
+ @where_clauses << clause
13
+
14
+ self
15
+ end
16
+
17
+ def where(condition)
18
+ clone.where!(condition)
19
+ end
20
+
21
+ def to_sql
22
+ sql = "SELECT * FROM #{@klass.table_name}"
23
+ sql += " WHERE #{@where_clauses.join(" AND ")}" if @where_clauses.any?
24
+
25
+ sql
26
+ end
27
+
28
+ def records
29
+ @records ||= @klass.find_by_sql(to_sql)
30
+ end
31
+
32
+ alias to_a records
33
+
34
+ def first
35
+ records.first
36
+ end
37
+
38
+ def each(&block)
39
+ records.each(&block)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Array
4
+ def to_sentence(delimiter: ", ", last_delimiter: "and")
5
+ return "" if none?
6
+ return self.first if one?
7
+
8
+ "#{self[0...-1].join(delimiter)} #{last_delimiter} #{self[-1]}"
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.expand_path("core_ext/**/*.rb", __dir__)].sort.each do |filepath|
4
+ require_relative filepath
5
+ end
@@ -14,5 +14,32 @@ module Jun
14
14
  def routes
15
15
  @routes ||= Jun::ActionDispatch::Routing::RouteSet.new
16
16
  end
17
+
18
+ def initialize!
19
+ return false if initialized? || Jun.application.nil?
20
+
21
+ # Add app/* directories to autoload paths.
22
+ Jun::ActiveSupport::Dependencies.autoload_paths += Jun.root.join("app").children
23
+
24
+ # Set up routes and make its helpers available to controllers & views.
25
+ require Jun.root.join("config/routes.rb")
26
+ url_helpers = Jun.application.routes.url_helpers
27
+ Jun::ActionController::Base.include(url_helpers)
28
+ Jun::ActionView::Base.include(url_helpers)
29
+
30
+ # Include all helpers in app/helpers directory.
31
+ Dir.glob(Jun.root.join("app/helpers/**/*.rb")).each do |filepath|
32
+ helper_class_name = File.basename(filepath, ".rb").camelize
33
+ helper_class = Object.const_get(helper_class_name)
34
+
35
+ Jun::ActionView::Base.include(helper_class)
36
+ end
37
+
38
+ @initialized = true
39
+ end
40
+
41
+ def initialized?
42
+ !!@initialized
43
+ end
17
44
  end
18
45
  end
@@ -6,7 +6,27 @@ module Jun
6
6
  module DB
7
7
  class Create < Base
8
8
  def process(*args)
9
- puts "Creating database..."
9
+ db_filepath = Jun.root.join("db/app.db")
10
+
11
+ if File.exist?(db_filepath)
12
+ puts "Database already exists."
13
+ else
14
+ File.open(db_filepath, "w") {}
15
+ create_schema_migrations_table
16
+ puts "Created database in #{db_filepath}."
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def create_schema_migrations_table
23
+ ActiveRecord::Base.connection.execute(
24
+ <<~SQL
25
+ CREATE TABLE IF NOT EXISTS schema_migrations (
26
+ version text NOT NULL PRIMARY KEY
27
+ );"
28
+ SQL
29
+ )
10
30
  end
11
31
  end
12
32
  end
@@ -6,7 +6,10 @@ module Jun
6
6
  module DB
7
7
  class Drop < Base
8
8
  def process(*args)
9
- puts "Dropping database..."
9
+ db_filepath = Jun.root.join("db/app.db")
10
+
11
+ File.delete(db_filepath) if File.exist?(db_filepath)
12
+ puts "Dropped database."
10
13
  end
11
14
  end
12
15
  end
@@ -6,7 +6,7 @@ module Jun
6
6
  module DB
7
7
  class Migrate < Base
8
8
  def process(*args)
9
- puts "Running migrations..."
9
+ ActiveRecord::Migrator.new(direction: :up).call
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jun
4
+ module CLI
5
+ module Commands
6
+ module DB
7
+ class Rollback < Base
8
+ def process(*args)
9
+ ActiveRecord::Migrator.new(direction: :down).call
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jun
4
+ module CLI
5
+ module Commands
6
+ module DB
7
+ module Schema
8
+ class Dump < Base
9
+ def process(*args)
10
+ schema_filepath = Jun.root.join("db/schema.sql")
11
+ db_filepath = Jun.root.join("db/app.db")
12
+
13
+ Bundler.with_original_env do
14
+ system("bundle exec sqlite3 #{db_filepath} .schema > #{schema_filepath}")
15
+ end
16
+
17
+ puts "Database schema updated."
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jun
4
+ module CLI
5
+ module Commands
6
+ module DB
7
+ module Schema
8
+ class Load < Base
9
+ def process(*args)
10
+ schema_filepath = Jun.root.join("db/schema.sql")
11
+ db_filepath = Jun.root.join("db/app.db")
12
+
13
+ Bundler.with_original_env do
14
+ system("bundle exec sqlite3 #{db_filepath} < #{schema_filepath}")
15
+ end
16
+
17
+ populate_schema_migrations!
18
+
19
+ puts "Database schema loaded."
20
+ end
21
+
22
+ private
23
+
24
+ def populate_schema_migrations!
25
+ migration_files = Dir.glob(Jun.root.join("db/migrate/*.rb")).sort
26
+
27
+ migration_files.each do |filepath|
28
+ filename = filepath.split("/").last.sub(".rb", "")
29
+ migration_version = filename.split("_").first
30
+
31
+ add_to_schema_migrations(migration_version)
32
+ end
33
+ end
34
+
35
+ def add_to_schema_migrations(version)
36
+ ActiveRecord::Base.connection.execute("INSERT INTO schema_migrations (version) VALUES (#{version});")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jun
4
+ module CLI
5
+ module Commands
6
+ module DB
7
+ class Seed < Base
8
+ def process(*args)
9
+ seed_filepath = Jun.root.join("db/seed.rb")
10
+ abort("No seed file found.") unless File.exist?(seed_filepath)
11
+
12
+ load seed_filepath
13
+ puts "Seeding complete."
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jun
4
+ module CLI
5
+ module Commands
6
+ module Generate
7
+ class Migration < Base
8
+ def process(*args)
9
+ migration_name = args.first
10
+
11
+ template_filepath = File.expand_path("../../generator_templates/migration.rb.erb", __dir__)
12
+ template = Tilt::ERBTemplate.new(template_filepath)
13
+ template_locals = { migration_name: migration_name }
14
+
15
+ filename = "#{Time.now.to_i}_#{migration_name}.rb"
16
+ filepath = Jun.root.join("db/migrate/#{filename}")
17
+ file_body = template.render(nil, template_locals)
18
+
19
+ filepath.dirname.mkpath
20
+ File.open(filepath, "w") { |f| f.write(file_body) }
21
+ puts "created #{filename}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -26,17 +26,20 @@ module Jun
26
26
  "README.md",
27
27
  "app/controllers/application_controller.rb",
28
28
  "app/helpers/application_helper.rb",
29
+ "app/models/application_record.rb",
29
30
  "app/views/layouts/application.html.erb",
31
+ "bin/console",
30
32
  "config/application.rb",
33
+ "config/environment.rb",
31
34
  "config/routes.rb",
32
- "db/app.db"
35
+ "db/seeds.rb"
33
36
  ]
34
37
 
35
38
  FileUtils.mkdir_p(app_name)
36
39
 
37
40
  FileUtils.chdir(app_name) do
38
41
  templates.each do |filepath|
39
- template_filepath = File.expand_path("../generators/new/#{filepath}.erb", __dir__)
42
+ template_filepath = File.expand_path("../generator_templates/new_app/#{filepath}.erb", __dir__)
40
43
  template = Tilt::ERBTemplate.new(template_filepath)
41
44
  template_locals = { app_name: app_name }
42
45
  file_body = template.render(nil, template_locals)
@@ -47,6 +50,8 @@ module Jun
47
50
  puts "created #{filepath}"
48
51
  end
49
52
 
53
+ FileUtils.chmod("u+x", "bin/console")
54
+
50
55
  puts "Installing dependencies..."
51
56
  Bundler.with_original_env { system("bundle install") }
52
57
  end
@@ -6,7 +6,7 @@ module Jun
6
6
  class Server < Base
7
7
  def process(*args)
8
8
  if Jun.root
9
- system("rerun --background -- rackup -p 3001")
9
+ system("rerun --background -- rackup -p 6291")
10
10
  else
11
11
  abort("Command \"#{self.class.command_name}\" must be run inside of a Jun app.")
12
12
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= migration_name.camelize %> < ActiveRecord::Migration
4
+ def up
5
+
6
+ end
7
+
8
+ def down
9
+
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ end
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "jun"
6
+
7
+ require "irb"
8
+ IRB.start(__FILE__)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jun"
4
+
5
+ # Require gems listed in the Gemfile, including any gems specified
6
+ # in the current environment group (e.g. :development, :production).
7
+ Bundler.require(*Jun.groups)
8
+
9
+ module <%= app_name.camelize %>
10
+ class Application < Jun::Application
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load the Jun application.
4
+ require_relative "application"
5
+
6
+ # Initialize the application.
7
+ Jun.application.initialize!
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/setup"
4
- require_relative "config/application"
5
- require_relative "config/routes"
4
+ require_relative "config/environment"
6
5
 
7
6
  run Jun.application
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is used by the `jun db:seed` command and should include
4
+ # any necessary logic for seeding the database.
5
+ #
6
+ # Example:
7
+ #
8
+ # User.create(name: "Sam", occupation: "author")
9
+ # Post.create(title: "How to Build a Bird House")
data/lib/jun/cli.rb CHANGED
@@ -11,7 +11,12 @@ module Jun
11
11
  Jun::CLI::Commands::New,
12
12
  Jun::CLI::Commands::DB::Create,
13
13
  Jun::CLI::Commands::DB::Migrate,
14
+ Jun::CLI::Commands::DB::Rollback,
15
+ Jun::CLI::Commands::DB::Seed,
14
16
  Jun::CLI::Commands::DB::Drop,
17
+ Jun::CLI::Commands::DB::Schema::Dump,
18
+ Jun::CLI::Commands::DB::Schema::Load,
19
+ Jun::CLI::Commands::Generate::Migration,
15
20
  Jun::CLI::Commands::Server,
16
21
  Jun::CLI::Commands::Version
17
22
  ].freeze
data/lib/jun/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jun
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/jun.rb CHANGED
@@ -3,10 +3,13 @@
3
3
  require "pathname"
4
4
 
5
5
  require_relative "jun/version"
6
- require_relative "jun/active_support/inflector"
6
+ require_relative "jun/active_support/core_ext"
7
7
  require_relative "jun/active_support/dependencies"
8
8
  require_relative "jun/action_dispatch/routing/route_set"
9
- require_relative "jun/active_record"
9
+ require_relative "jun/active_record/base"
10
+ require_relative "jun/active_record/migration"
11
+ require_relative "jun/active_record/migrator"
12
+ require_relative "jun/active_record/relation"
10
13
  require_relative "jun/action_controller/base"
11
14
  require_relative "jun/application"
12
15
 
@@ -22,6 +25,14 @@ module Jun
22
25
  project_root_path
23
26
  end
24
27
 
28
+ def env
29
+ ENV["JUN_ENV"] || ENV["RACK_ENV"] || "development"
30
+ end
31
+
32
+ def groups
33
+ [:default, env]
34
+ end
35
+
25
36
  private
26
37
 
27
38
  def project_root_path
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jun
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zoran
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-03-16 00:00:00.000000000 Z
11
+ date: 2022-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -108,7 +108,7 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0.14'
111
- description: A simple web framework inspired by Rails.
111
+ description: A simple web framework inspired by Rails. Not meant for production use.
112
112
  email:
113
113
  - zspesic@gmail.com
114
114
  executables:
@@ -127,33 +127,52 @@ files:
127
127
  - jun.gemspec
128
128
  - lib/jun.rb
129
129
  - lib/jun/action_controller/base.rb
130
+ - lib/jun/action_controller/callbacks.rb
131
+ - lib/jun/action_controller/metal.rb
132
+ - lib/jun/action_controller/redirecting.rb
130
133
  - lib/jun/action_controller/rendering.rb
131
134
  - lib/jun/action_dispatch/routing/mapper.rb
132
135
  - lib/jun/action_dispatch/routing/route_set.rb
136
+ - lib/jun/action_dispatch/routing/welcome.html.erb
133
137
  - lib/jun/action_view/base.rb
134
138
  - lib/jun/action_view/helpers.rb
135
139
  - lib/jun/action_view/helpers/url_helper.rb
136
- - lib/jun/active_record.rb
140
+ - lib/jun/active_record/base.rb
141
+ - lib/jun/active_record/migration.rb
142
+ - lib/jun/active_record/migrator.rb
143
+ - lib/jun/active_record/persistence.rb
144
+ - lib/jun/active_record/relation.rb
145
+ - lib/jun/active_support/core_ext.rb
146
+ - lib/jun/active_support/core_ext/array/conversion.rb
147
+ - lib/jun/active_support/core_ext/string/inflector.rb
137
148
  - lib/jun/active_support/dependencies.rb
138
- - lib/jun/active_support/inflector.rb
139
149
  - lib/jun/application.rb
140
150
  - lib/jun/cli.rb
141
151
  - lib/jun/cli/commands/base.rb
142
152
  - lib/jun/cli/commands/db/create.rb
143
153
  - lib/jun/cli/commands/db/drop.rb
144
154
  - lib/jun/cli/commands/db/migrate.rb
155
+ - lib/jun/cli/commands/db/rollback.rb
156
+ - lib/jun/cli/commands/db/schema/dump.rb
157
+ - lib/jun/cli/commands/db/schema/load.rb
158
+ - lib/jun/cli/commands/db/seed.rb
159
+ - lib/jun/cli/commands/generate/migration.rb
145
160
  - lib/jun/cli/commands/new.rb
146
161
  - lib/jun/cli/commands/server.rb
147
162
  - lib/jun/cli/commands/version.rb
148
- - lib/jun/cli/generators/new/Gemfile.erb
149
- - lib/jun/cli/generators/new/README.md.erb
150
- - lib/jun/cli/generators/new/app/controllers/application_controller.rb.erb
151
- - lib/jun/cli/generators/new/app/helpers/application_helper.rb.erb
152
- - lib/jun/cli/generators/new/app/views/layouts/application.html.erb.erb
153
- - lib/jun/cli/generators/new/config.ru.erb
154
- - lib/jun/cli/generators/new/config/application.rb.erb
155
- - lib/jun/cli/generators/new/config/routes.rb.erb
156
- - lib/jun/cli/generators/new/db/app.db.erb
163
+ - lib/jun/cli/generator_templates/migration.rb.erb
164
+ - lib/jun/cli/generator_templates/new_app/Gemfile.erb
165
+ - lib/jun/cli/generator_templates/new_app/README.md.erb
166
+ - lib/jun/cli/generator_templates/new_app/app/controllers/application_controller.rb.erb
167
+ - lib/jun/cli/generator_templates/new_app/app/helpers/application_helper.rb.erb
168
+ - lib/jun/cli/generator_templates/new_app/app/models/application_record.rb.erb
169
+ - lib/jun/cli/generator_templates/new_app/app/views/layouts/application.html.erb.erb
170
+ - lib/jun/cli/generator_templates/new_app/bin/console.erb
171
+ - lib/jun/cli/generator_templates/new_app/config.ru.erb
172
+ - lib/jun/cli/generator_templates/new_app/config/application.rb.erb
173
+ - lib/jun/cli/generator_templates/new_app/config/environment.rb.erb
174
+ - lib/jun/cli/generator_templates/new_app/config/routes.rb.erb
175
+ - lib/jun/cli/generator_templates/new_app/db/seeds.rb.erb
157
176
  - lib/jun/connection_adapters/sqlite_adapter.rb
158
177
  - lib/jun/version.rb
159
178
  homepage: https://github.com/zokioki/jun
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "jun"
4
-
5
- Jun::ActiveSupport::Dependencies.autoload_paths += Jun.root.join("app").children
6
-
7
- # Include all helpers in app/helpers directory
8
- Dir.glob(Jun.root.join("app/helpers/**/*.rb")).each do |filepath|
9
- helper_class_name = File.basename(filepath, ".rb").camelize
10
- helper_class = Object.const_get(helper_class_name)
11
-
12
- Jun::ActionView::Base.include(helper_class)
13
- end
14
-
15
- module <%= app_name.camelize %>
16
- class Application < Jun::Application
17
- end
18
- end
File without changes