jun 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -15
  3. data/jun.gemspec +4 -4
  4. data/lib/jun/action_controller/base.rb +6 -10
  5. data/lib/jun/action_controller/callbacks.rb +46 -0
  6. data/lib/jun/action_controller/metal.rb +22 -0
  7. data/lib/jun/action_controller/redirecting.rb +17 -0
  8. data/lib/jun/action_controller/rendering.rb +0 -4
  9. data/lib/jun/action_dispatch/routing/mapper.rb +28 -3
  10. data/lib/jun/action_dispatch/routing/route_set.rb +49 -2
  11. data/lib/jun/action_dispatch/routing/welcome.html.erb +59 -0
  12. data/lib/jun/{active_record.rb → active_record/base.rb} +14 -3
  13. data/lib/jun/active_record/migration.rb +76 -0
  14. data/lib/jun/active_record/migrator.rb +80 -0
  15. data/lib/jun/active_record/persistence.rb +60 -0
  16. data/lib/jun/active_record/relation.rb +42 -0
  17. data/lib/jun/active_support/core_ext/array/access.rb +33 -0
  18. data/lib/jun/active_support/core_ext/array/conversion.rb +18 -0
  19. data/lib/jun/active_support/core_ext/hash/transformation.rb +29 -0
  20. data/lib/jun/active_support/core_ext/string/access.rb +42 -0
  21. data/lib/jun/active_support/{inflector.rb → core_ext/string/inflector.rb} +18 -0
  22. data/lib/jun/active_support/core_ext.rb +5 -0
  23. data/lib/jun/active_support/dependencies.rb +2 -0
  24. data/lib/jun/application.rb +31 -0
  25. data/lib/jun/cli/commands/db/create.rb +21 -1
  26. data/lib/jun/cli/commands/db/drop.rb +4 -1
  27. data/lib/jun/cli/commands/db/migrate.rb +1 -1
  28. data/lib/jun/cli/commands/db/rollback.rb +15 -0
  29. data/lib/jun/cli/commands/db/schema/dump.rb +24 -0
  30. data/lib/jun/cli/commands/db/schema/load.rb +43 -0
  31. data/lib/jun/cli/commands/db/seed.rb +19 -0
  32. data/lib/jun/cli/commands/generate/migration.rb +27 -0
  33. data/lib/jun/cli/commands/new.rb +7 -2
  34. data/lib/jun/cli/commands/server.rb +1 -1
  35. data/lib/jun/cli/generator_templates/migration.rb.erb +11 -0
  36. data/lib/jun/cli/{generators/new → generator_templates/new_app}/Gemfile.erb +0 -0
  37. data/lib/jun/cli/{generators/new → generator_templates/new_app}/README.md.erb +0 -0
  38. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/controllers/application_controller.rb.erb +0 -0
  39. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/helpers/application_helper.rb.erb +0 -0
  40. data/lib/jun/cli/generator_templates/new_app/app/models/application_record.rb.erb +4 -0
  41. data/lib/jun/cli/{generators/new → generator_templates/new_app}/app/views/layouts/application.html.erb.erb +0 -0
  42. data/lib/jun/cli/generator_templates/new_app/bin/console.erb +8 -0
  43. data/lib/jun/cli/generator_templates/new_app/config/application.rb.erb +12 -0
  44. data/lib/jun/cli/generator_templates/new_app/config/environment.rb.erb +7 -0
  45. data/lib/jun/cli/{generators/new → generator_templates/new_app}/config/routes.rb.erb +0 -0
  46. data/lib/jun/cli/{generators/new → generator_templates/new_app}/config.ru.erb +1 -2
  47. data/lib/jun/cli/generator_templates/new_app/db/seeds.rb.erb +9 -0
  48. data/lib/jun/cli.rb +5 -0
  49. data/lib/jun/version.rb +1 -1
  50. data/lib/jun.rb +13 -2
  51. metadata +48 -26
  52. data/lib/jun/cli/generators/new/config/application.rb.erb +0 -18
  53. data/lib/jun/cli/generators/new/db/app.db.erb +0 -0
@@ -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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Array
4
+ # Returns the second element in the array.
5
+ def second
6
+ self[1]
7
+ end
8
+
9
+ # Returns the third element in the array.
10
+ def third
11
+ self[2]
12
+ end
13
+
14
+ # Returns the fourth element in the array.
15
+ def fourth
16
+ self[3]
17
+ end
18
+
19
+ # Returns the fifth element in the array.
20
+ def fifth
21
+ self[4]
22
+ end
23
+
24
+ # Returns the third-to-last element in the array.
25
+ def third_to_last
26
+ self[-3]
27
+ end
28
+
29
+ # Returns the second-to-last element in the array.
30
+ def second_to_last
31
+ self[-2]
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Array
4
+ # Converts the array to a comma-separated sentence.
5
+ #
6
+ # ["one", "two", "three"].to_sentence #=> "one, two and three"
7
+ # ["left", "right"].to_sentence(last_delimiter: "or") #=> "left or right"
8
+ # [].to_sentence #=> ""
9
+ #
10
+ # @param delimiter [String] the delimiter value for connecting array elements.
11
+ # @param last_delimiter [String] the connecting word for the last array element.
12
+ def to_sentence(delimiter: ", ", last_delimiter: "and")
13
+ return "" if none?
14
+ return self.first if one?
15
+
16
+ "#{self[0...-1].join(delimiter)} #{last_delimiter} #{self[-1]}"
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hash
4
+ # Returns a new hash with all keys converted to strings.
5
+ #
6
+ # { name: "Tom", age: 50 }.stringify_keys #=> {"name"=>"Tom", "age"=>50}
7
+ def stringify_keys
8
+ transform_keys(&:to_s)
9
+ end
10
+
11
+ # Modifies the hash in place to convert all keys to strings.
12
+ # Same as +stringify_keys+ but modifies +self+.
13
+ def stringify_keys!
14
+ transform_keys!(&:to_s)
15
+ end
16
+
17
+ # Returns a new hash with all keys converted to symbols.
18
+ #
19
+ # { "name" => "Tom", "age" => 50 }.symbolize_keys #=> {:name=>"Tom", :age=>50 }
20
+ def symbolize_keys
21
+ transform_keys { |key| key.to_sym rescue key }
22
+ end
23
+
24
+ # Modifies the hash in place to convert all keys to symbols.
25
+ # Same as +symbolize_keys+ but modifies +self+.
26
+ def symbolize_keys!
27
+ transform_keys! { |key| key.to_sym rescue key }
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ # Returns a character at the given integer of the string. The first character
5
+ # would be returned for index 0, the second at index 1, and onward. If a range
6
+ # is given, a substring conaining the characters within the range of the given
7
+ # indicies is returned. If a Regex is provided, the matching substring is returned.
8
+ #
9
+ # string = "smoki"
10
+ # string.at(0) # => "s"
11
+ # string.at(1..3) # => "mok"
12
+ # string.at(-2) # => "k"
13
+ # string.at(/oki/) # => "oki"
14
+ def at(position)
15
+ self[position]
16
+ end
17
+
18
+ # Returns a substring from the given position (index) to the end of the string.
19
+ # If the position is negative, the starting point is counted from the end of the string.
20
+ #
21
+ # string = "smoki"
22
+ # string.from(0) # => "smoki"
23
+ # string.from(3) # => "ki"
24
+ # string.from(-2) # => "ki"
25
+ def from(position)
26
+ self[position, length]
27
+ end
28
+
29
+ # Returns a substring from the beginning of the string to the given position (index).
30
+ # If the position is negative, the ending point is counted from the end of the string.
31
+ #
32
+ # string = "smoki"
33
+ # string.to(0) # => "s"
34
+ # string.to(3) # => "smok"
35
+ # string.to(-2) # => "smok"
36
+ def to(position)
37
+ position += size if position.negative?
38
+ position = -1 if position.negative?
39
+
40
+ self[0, position + 1]
41
+ end
42
+ end
@@ -68,6 +68,11 @@ module Inflector
68
68
  'equipment'
69
69
  ]
70
70
 
71
+ # Returns the plural form of the string.
72
+ #
73
+ # "task".pluralize #=> "tasks"
74
+ # "octopus".pluralize #=> "octopi"
75
+ # "fish".singularize #=> "fish"
71
76
  def pluralize
72
77
  return self if UNCHANGEABLE.include? self
73
78
 
@@ -82,6 +87,11 @@ module Inflector
82
87
  self.sub(pattern, replacement)
83
88
  end
84
89
 
90
+ # Returns the singular form of the string.
91
+ #
92
+ # "tasks".singularize #=> "task"
93
+ # "octopi".singularize #=> "octopus"
94
+ # "fish".singularize #=> "fish"
85
95
  def singularize
86
96
  return self if UNCHANGEABLE.include? self
87
97
 
@@ -96,6 +106,10 @@ module Inflector
96
106
  self.sub(pattern, replacement)
97
107
  end
98
108
 
109
+ # Converts a string to under_score format.
110
+ #
111
+ # "HelloThere".underscore #=> "hello_there"
112
+ # "Foo::BarBaz".underscore #=> "foo/bar_baz"
99
113
  def underscore
100
114
  gsub(/::/, '/').
101
115
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
@@ -104,6 +118,10 @@ module Inflector
104
118
  downcase
105
119
  end
106
120
 
121
+ # Converts a string to CamelCase format.
122
+ #
123
+ # "hello_there".camelize #=> "HelloThere"
124
+ # "foo/bar_baz".camelize #=> "Foo::BarBaz"
107
125
  def camelize
108
126
  sub(/^[a-z\d]*/) { |match| match.capitalize }.
109
127
  gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.
@@ -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
@@ -8,6 +8,8 @@ module Jun
8
8
  attr_accessor :autoload_paths
9
9
  self.autoload_paths = []
10
10
 
11
+ # Returns the filepath for a given filename if the file exists
12
+ # in one of the directories specified in +autoload_paths+.
11
13
  def find_file(filename)
12
14
  autoload_paths.each do |path|
13
15
  filepath = File.join(path, "#{filename}.rb")
@@ -14,5 +14,36 @@ module Jun
14
14
  def routes
15
15
  @routes ||= Jun::ActionDispatch::Routing::RouteSet.new
16
16
  end
17
+
18
+ # Initializes the Jun application.
19
+ # @return [Boolean] +true+ if successful, +false+ if already initialized.
20
+ def initialize!
21
+ return false if initialized? || Jun.application.nil?
22
+
23
+ # Add app/* directories to autoload paths.
24
+ Jun::ActiveSupport::Dependencies.autoload_paths += Jun.root.join("app").children
25
+
26
+ # Set up routes and make its helpers available to controllers & views.
27
+ require Jun.root.join("config/routes.rb")
28
+ url_helpers = Jun.application.routes.url_helpers
29
+ Jun::ActionController::Base.include(url_helpers)
30
+ Jun::ActionView::Base.include(url_helpers)
31
+
32
+ # Include all helpers in app/helpers directory.
33
+ Dir.glob(Jun.root.join("app/helpers/**/*.rb")).each do |filepath|
34
+ helper_class_name = File.basename(filepath, ".rb").camelize
35
+ helper_class = Object.const_get(helper_class_name)
36
+
37
+ Jun::ActionView::Base.include(helper_class)
38
+ end
39
+
40
+ @initialized = true
41
+ end
42
+
43
+ # Checks whether the Jun application has been initialized.
44
+ # @return [Boolean] +true+ if initialized, +false+ if not initialized.
45
+ def initialized?
46
+ !!@initialized
47
+ end
17
48
  end
18
49
  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.3.1"
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