jun 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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