rage-rb 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88726c2fad086904077b7822723c8f840fee5beac920f21d08862c637c2c4b42
4
- data.tar.gz: 37520e426a1036244dab498bbc3bb7c4155c2886ad3ecc3293f376d4dbcd6390
3
+ metadata.gz: 3aacbd2e65fda6a0e7a7f7a24e67f718e55fd66bd3e0a6c6d3a1a319294be44b
4
+ data.tar.gz: 40d3d2225cf40bf0f125884afd99a86b47b34f6dddce5fdeea0a9b5239fdfd0a
5
5
  SHA512:
6
- metadata.gz: 1d635e49fc41cc68889ff1605ee1055e5ef443987b310730d91c675ca96083115f8de871a7f6d738535ee98790d2bd8b540aa476f18676fcbf6592dc719e60db
7
- data.tar.gz: cdd975b3be0832d841455d736bdf1caa23d916e357ad8d20b641303b00d34dd2711fc8de34cb9a5488020ec59ec283aafc1aacb990119c075565b7c35dbfcfdd
6
+ metadata.gz: 288d8f707d78901e9c209cc3af075bcf3c37c7ef0ff4c24355702bd3898ab2e041afc5ab028e4cbe09e26a95f7297e01af03430ac6f126fd5d6cfa0b772f0e5e
7
+ data.tar.gz: 5c70306e1901176e5da774dcb153e21c382359825a2527a7de97ce75772dfdf6a510ac3ffba05486b684b55634a4173596d9469306571acf58ffff274a6185c9
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --exclude lib/rage/templates --markup markdown --no-private
data/Gemfile CHANGED
@@ -8,3 +8,6 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
+
12
+ gem "pg"
13
+ gem "mysql2"
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <p align="center"><img height="200" src="https://github.com/rage-rb/rage/assets/2270393/d0f0834f-50e4-4b1b-a564-1f241c4ec149" /></p>
2
+
1
3
  # Rage
2
4
 
3
5
  [![Gem Version](https://badge.fury.io/rb/rage-rb.svg)](https://badge.fury.io/rb/rage-rb)
@@ -5,16 +7,14 @@
5
7
 
6
8
  Inspired by [Deno](https://deno.com) and built on top of [Iodine](https://github.com/rage-rb/iodine), this is a Ruby web framework that is based on the following design principles:
7
9
 
8
- * **Rails compatible API** - Rails' API is clean, straightforward, and simply makes sense. I believe it was one of the reasons why Rails was so successful in the past.
10
+ * **Rails compatible API** - Rails' API is clean, straightforward, and simply makes sense. It was one of the reasons why Rails was so successful in the past.
9
11
 
10
- * **High performance** - some think performance is not a major metric for a framework, but I don't believe it's true. Poor performance is a risk, and in today's world, companies refuse to use risky technologies.
12
+ * **High performance** - some think performance is not a major metric for a framework, but it's not true. Poor performance is a risk, and in today's world, companies refuse to use risky technologies.
11
13
 
12
- * **API-only** - the only technology we should be using to create web UI is JavaScript. I recommend checking out [Vite](https://vitejs.dev) if you don't know where to start.
14
+ * **API-only** - the only technology we should be using to create web UI is JavaScript. Check out [Vite](https://vitejs.dev) if you don't know where to start.
13
15
 
14
16
  * **Acceptance of modern Ruby** - the framework includes a fiber scheduler, which means your code never blocks while waiting on IO.
15
17
 
16
- This framework results from reflecting on [Ruby's declining popularity](https://survey.stackoverflow.co/2023/#most-popular-technologies-language) and attempting to answer why this is happening and what we, as a community, could be doing differently.
17
-
18
18
  ## Installation
19
19
 
20
20
  Install the gem:
@@ -40,6 +40,76 @@ $ rage s
40
40
 
41
41
  Start coding!
42
42
 
43
+ ## Getting Started
44
+
45
+ This gem is designed to be a drop-in replacement for Rails in API mode. Public API is mostly expected to match Rails, however, sometimes it's a little bit more strict.
46
+
47
+ Check out in-depth API docs for more information:
48
+
49
+ - [Controller API](https://rage-rb.github.io/rage/RageController/API.html)
50
+ - [Routing API](https://rage-rb.github.io/rage/Rage/Router/DSL/Handler.html)
51
+ - [Fiber API](https://rage-rb.github.io/rage/Fiber.html)
52
+
53
+ Also, see the [changelog](https://github.com/rage-rb/rage/blob/master/CHANGELOG.md) and [upcoming-releases](https://github.com/rage-rb/rage#upcoming-releases) for currently supported and planned features.
54
+
55
+ ### Example
56
+
57
+ A sample controller could look like this:
58
+
59
+ ```ruby
60
+ require "net/http"
61
+
62
+ class PagesController < RageController::API
63
+ rescue_from SocketError do |_|
64
+ render json: { message: "error" }, status: 500
65
+ end
66
+
67
+ before_action :set_metadata
68
+
69
+ def show
70
+ page = Net::HTTP.get(URI("https://httpbin.org/json"))
71
+ render json: { page: page, metadata: @metadata }
72
+ end
73
+
74
+ private
75
+
76
+ def set_metadata
77
+ @metadata = { format: "json", time: Time.now.to_i }
78
+ end
79
+ end
80
+ ```
81
+
82
+ Apart from `RageController::API` as a parent class, this is mostly a regular Rails controller. However, the main difference is under the hood - Rage runs every request in a separate fiber. During the call to `Net::HTTP.get`, the fiber is automatically paused, enabling the server to process other requests. Once the HTTP request is finished, the fiber will be resumed, potentially allowing to process hundreds of requests simultaneously.
83
+
84
+ To make this controller work, we would also need to update `config/routes.rb`. In this case, the file would look the following way:
85
+
86
+ ```ruby
87
+ Rage.routes.draw do
88
+ get "page", to: "pages#show"
89
+ end
90
+ ```
91
+
92
+ :information_source: **Note**: Rage will automatically pause a fiber and continue to process other fibers on HTTP, PostgreSQL, and MySQL calls. Calls to `Thread.join` and `Ractor.join` will also automatically pause the current fiber.
93
+
94
+ Additionally, `Fiber.await` can be used to run several requests in parallel:
95
+
96
+ ```ruby
97
+ require "net/http"
98
+
99
+ class PagesController < RageController::API
100
+ def index
101
+ pages = Fiber.await(
102
+ Fiber.schedule { Net::HTTP.get(URI("https://httpbin.org/json")) },
103
+ Fiber.schedule { Net::HTTP.get(URI("https://httpbin.org/html")) },
104
+ )
105
+
106
+ render json: { pages: pages }
107
+ end
108
+ end
109
+ ```
110
+
111
+ :information_source: **Note**: When using `Fiber.await`, it is important to wrap any instance of IO into a fiber using `Fiber.schedule`.
112
+
43
113
  ## Benchmarks
44
114
 
45
115
  #### hello world
@@ -51,7 +121,7 @@ class ArticlesController < ApplicationController
51
121
  end
52
122
  end
53
123
  ```
54
- ![Requests per second](https://github.com/rage-rb/rage/assets/2270393/7d9f408c-7cec-4cc0-a509-66c9dedc1d0a)
124
+ ![Requests per second](https://github.com/rage-rb/rage/assets/2270393/6c221903-e265-4c94-80e1-041f266c8f47)
55
125
 
56
126
  #### waiting on IO
57
127
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  class Rage::Application
4
4
  def initialize(router)
5
- Fiber.set_scheduler(Rage::FiberScheduler.new)
5
+ Iodine.on_state(:on_start) do
6
+ Fiber.set_scheduler(Rage::FiberScheduler.new)
7
+ end
6
8
  @router = router
7
9
  end
8
10
 
data/lib/rage/cli.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require "iodine"
5
- require "rack"
4
+ require "rage"
6
5
 
7
6
  module Rage
8
7
  class CLI < Thor
@@ -20,8 +19,10 @@ module Rage
20
19
  app = ::Rack::Builder.parse_file("config.ru")
21
20
  app = app[0] if app.is_a?(Array)
22
21
 
23
- ::Iodine.listen service: :http, handler: app
24
- ::Iodine.threads = 1
22
+ ::Iodine.listen service: :http, handler: app, port: Rage.config.port
23
+ ::Iodine.threads = Rage.config.threads_count
24
+ ::Iodine.workers = Rage.config.workers_count
25
+
25
26
  ::Iodine.start
26
27
  end
27
28
  end
@@ -0,0 +1,10 @@
1
+ class Rage::Configuration
2
+ attr_accessor :port, :workers_count
3
+ attr_reader :threads_count
4
+
5
+ def initialize
6
+ @threads_count = 1
7
+ @workers_count = -1
8
+ @port = 3000
9
+ end
10
+ end
@@ -2,6 +2,11 @@
2
2
 
3
3
  class RageController::API
4
4
  class << self
5
+ # @private
6
+ # used by the router to register a new action;
7
+ # registering means defining a new method which calls the action, makes additional calls (e.g. before actions) and
8
+ # sends a correct response down to the server;
9
+ # returns the name of the newly defined method;
5
10
  def __register_action(action)
6
11
  raise "The action '#{action}' could not be found for #{self}" unless method_defined?(action)
7
12
 
@@ -23,37 +28,143 @@ class RageController::API
23
28
  ""
24
29
  end
25
30
 
31
+ rescue_handlers_chunk = if @__rescue_handlers
32
+ lines = @__rescue_handlers.map do |klasses, handler|
33
+ <<-RUBY
34
+ rescue #{klasses.join(", ")} => __e
35
+ #{handler}(__e)
36
+ [@__status, @__headers, @__body]
37
+ RUBY
38
+ end
39
+
40
+ lines.join("\n")
41
+ else
42
+ ""
43
+ end
44
+
26
45
  class_eval <<-RUBY
27
46
  def __run_#{action}
28
47
  #{before_actions_chunk}
29
48
  #{action}
30
49
 
31
50
  [@__status, @__headers, @__body]
51
+
52
+ #{rescue_handlers_chunk}
32
53
  end
33
54
  RUBY
34
55
  end
35
56
 
36
- # Register a new `before_action` hook.
57
+ # @private
58
+ attr_writer :__before_actions, :__rescue_handlers
59
+
60
+ # @private
61
+ # pass the variable down to the child; the child will continue to use it until changes need to be made;
62
+ # only then the object will be copied; the frozen state communicates that the object is shared with the parent;
63
+ def inherited(klass)
64
+ klass.__before_actions = @__before_actions.freeze
65
+ klass.__rescue_handlers = @__rescue_handlers.freeze
66
+ end
67
+
68
+ ############
69
+ #
70
+ # PUBLIC API
71
+ #
72
+ ############
73
+
74
+ # Register a global exception handler. Handlers are inherited and matched from bottom to top.
75
+ #
76
+ # @param klasses [Class, Array<Class>] exception classes to watch on
77
+ # @param with [Symbol] the name of a handler method. The method must take one argument, which is the raised exception. Alternatively, you can pass a block, which must also take one argument.
78
+ # @example
79
+ # rescue_from User::NotAuthorized, with: :deny_access
80
+ #
81
+ # def deny_access(exception)
82
+ # head :forbidden
83
+ # end
84
+ # @example
85
+ # rescue_from User::NotAuthorized do |_|
86
+ # head :forbidden
87
+ # end
88
+ # @note Unlike Rails, the handler must always take an argument. Use `_` if you don't care about the actual exception.
89
+ def rescue_from(*klasses, with: nil, &block)
90
+ unless with
91
+ if block_given?
92
+ name = ("a".."z").to_a.sample(15).join
93
+ with = define_method("__#{name}", &block)
94
+ else
95
+ raise "No handler provided. Pass the `with` keyword argument or provide a block."
96
+ end
97
+ end
98
+
99
+ if @__rescue_handlers.nil?
100
+ @__rescue_handlers = []
101
+ elsif @__rescue_handlers.frozen?
102
+ @__rescue_handlers = @__rescue_handlers.dup
103
+ end
104
+
105
+ @__rescue_handlers.unshift([klasses, with])
106
+ end
107
+
108
+ # Register a new `before_action` hook. Calls with the same `action_name` will overwrite the previous ones.
37
109
  #
38
110
  # @param action_name [String] the name of the callback to add
39
111
  # @param only [Symbol, Array<Symbol>] restrict the callback to run only for specific actions
40
112
  # @param except [Symbol, Array<Symbol>] restrict the callback to run for all actions except specified
41
113
  # @example
42
114
  # before_action :find_photo, only: :show
115
+ #
43
116
  # def find_photo
44
- # ...
117
+ # Photo.first
45
118
  # end
46
119
  def before_action(action_name, only: nil, except: nil)
47
- (@__before_actions ||= []) << {
48
- name: action_name,
49
- only: only && Array(only),
50
- except: except && Array(except)
51
- }
120
+ if @__before_actions && @__before_actions.frozen?
121
+ @__before_actions = @__before_actions.dup
122
+ end
123
+
124
+ action = { name: action_name, only: only && Array(only), except: except && Array(except) }
125
+ if @__before_actions.nil?
126
+ @__before_actions = [action]
127
+ elsif i = @__before_actions.find_index { |a| a[:name] == action_name }
128
+ @__before_actions[i] = action
129
+ else
130
+ @__before_actions << action
131
+ end
132
+ end
133
+
134
+ # Prevent a `before_action` hook from running.
135
+ #
136
+ # @param action_name [String] the name of the callback to skip
137
+ # @param only [Symbol, Array<Symbol>] restrict the callback to be skipped only for specific actions
138
+ # @param except [Symbol, Array<Symbol>] restrict the callback to be skipped for all actions except specified
139
+ # @example
140
+ # skip_before_action :find_photo, only: :create
141
+ def skip_before_action(action_name, only: nil, except: nil)
142
+ i = @__before_actions&.find_index { |a| a[:name] == action_name }
143
+ raise "The following action was specified to be skipped but cannot be found: #{self}##{action_name}" unless i
144
+
145
+ @__before_actions = @__before_actions.dup if @__before_actions.frozen?
146
+
147
+ if only.nil? && except.nil?
148
+ @__before_actions.delete_at(i)
149
+ return
150
+ end
151
+
152
+ action = @__before_actions[i].dup
153
+ if only
154
+ action[:except] ? action[:except] |= Array(only) : action[:except] = Array(only)
155
+ end
156
+ if except
157
+ action[:only] = Array(except)
158
+ end
159
+
160
+ @__before_actions[i] = action
52
161
  end
53
162
  end # class << self
54
163
 
164
+ # @private
55
165
  DEFAULT_HEADERS = { "content-type" => "application/json; charset=utf-8" }.freeze
56
166
 
167
+ # @private
57
168
  def initialize(env, params)
58
169
  @__env = env
59
170
  @__params = params
@@ -72,7 +183,7 @@ class RageController::API
72
183
  # render status: :ok
73
184
  # @example
74
185
  # render plain: "hello world", status: 201
75
- # @note `render` doesn't terminate execution of the action, so if you want to exit an action after rendering, you need to do something like 'render(...) and return'
186
+ # @note `render` doesn't terminate execution of the action, so if you want to exit an action after rendering, you need to do something like `render(...) and return`.
76
187
  def render(json: nil, plain: nil, status: nil)
77
188
  raise "Render was called multiple times in this action" if @__rendered
78
189
  @__rendered = true
@@ -81,7 +192,7 @@ class RageController::API
81
192
  @__body << if json
82
193
  json.is_a?(String) ? json : json.to_json
83
194
  else
84
- set_header("content-type", "text/plain; charset=utf-8")
195
+ __set_header("content-type", "text/plain; charset=utf-8")
85
196
  plain
86
197
  end
87
198
 
@@ -116,7 +227,8 @@ class RageController::API
116
227
 
117
228
  private
118
229
 
119
- def set_header(key, value)
230
+ # copy-on-write implementation for the headers object
231
+ def __set_header(key, value)
120
232
  @__headers = @__headers.dup if DEFAULT_HEADERS.equal?(@__headers)
121
233
  @__headers[key] = value
122
234
  end
data/lib/rage/fiber.rb CHANGED
@@ -1,9 +1,38 @@
1
1
  class Fiber
2
+ # @private
2
3
  def __set_result(result)
3
4
  @__result = result
4
5
  end
5
6
 
7
+ # @private
6
8
  def __get_result
7
9
  @__result
8
10
  end
11
+
12
+ # Wait on several fibers at the same time. Calling this method will automatically pause the current fiber, allowing the
13
+ # server to process other requests. Once all fibers have completed, the current fiber will be automatically resumed.
14
+ #
15
+ # @param fibers [Fiber, Array<Fiber>] one or several fibers to wait on. The fibers must be created using the `Fiber.schedule` call.
16
+ # @example
17
+ # Fiber.await(
18
+ # Fiber.schedule { request_1 },
19
+ # Fiber.schedule { request_2 },
20
+ # )
21
+ # @note This method should only be used when multiple fibers have to be processed in parallel. There's no need to use `Fiber.await` for single IO calls.
22
+ def self.await(*fibers)
23
+ f = Fiber.current
24
+
25
+ num_wait_for = fibers.count(&:alive?)
26
+ return fibers.map(&:__get_result) if num_wait_for == 0
27
+
28
+ Iodine.subscribe("await:#{f.object_id}") do
29
+ num_wait_for -= 1
30
+ f.resume if num_wait_for == 0
31
+ end
32
+
33
+ Fiber.yield
34
+ Iodine.defer { Iodine.unsubscribe("await:#{f.object_id}") }
35
+
36
+ fibers.map(&:__get_result)
37
+ end
9
38
  end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "resolv"
4
+
3
5
  class Rage::FiberScheduler
6
+ def initialize
7
+ @root_fiber = Fiber.current
8
+ end
9
+
4
10
  def io_wait(io, events, timeout = nil)
5
11
  f = Fiber.current
6
12
  ::Iodine::Scheduler.attach(io.fileno, events, timeout&.ceil || 0) { f.resume }
@@ -84,8 +90,14 @@ class Rage::FiberScheduler
84
90
  end
85
91
 
86
92
  def fiber(&block)
93
+ f = Fiber.current
94
+ inner_schedule = f != @root_fiber
95
+
87
96
  fiber = Fiber.new(blocking: false) do
88
97
  Fiber.current.__set_result(block.call)
98
+ ensure
99
+ # send a message for `Fiber.await` to work
100
+ Iodine.publish("await:#{f.object_id}", "") if inner_schedule
89
101
  end
90
102
  fiber.resume
91
103
 
@@ -21,8 +21,8 @@ class Rage::Router::Backend
21
21
  path_full = path.sub(OPTIONAL_PARAM_REGEXP, "/#{$1}")
22
22
  path_optional = path.sub(OPTIONAL_PARAM_REGEXP, "")
23
23
 
24
- on(method, path_full, handler)
25
- on(method, path_optional, handler)
24
+ on(method, path_full, handler, constraints: constraints)
25
+ on(method, path_optional, handler, constraints: constraints)
26
26
  return
27
27
  end
28
28
 
@@ -166,6 +166,7 @@ class Rage::Router::Backend
166
166
 
167
167
  def find(env, derived_constraints)
168
168
  method, path = env["REQUEST_METHOD"], env["PATH_INFO"]
169
+ path.delete_suffix!("/") if path.end_with?("/") && path.length > 1
169
170
 
170
171
  current_node = @trees[method]
171
172
  return nil unless current_node
@@ -10,6 +10,7 @@ class Rage::Router::DSL
10
10
  end
11
11
 
12
12
  class Handler
13
+ # @private
13
14
  def initialize(router)
14
15
  @router = router
15
16
 
@@ -122,6 +123,10 @@ class Rage::Router::DSL
122
123
  path = path.delete_suffix("/") if path.end_with?("/")
123
124
  end
124
125
 
126
+ if path == "/" && @path_prefixes.any?
127
+ path = ""
128
+ end
129
+
125
130
  path_prefix = @path_prefixes.any? ? "/#{@path_prefixes.join("/")}" : nil
126
131
  module_prefix = @module_prefixes.any? ? "#{@module_prefixes.join("/")}/" : nil
127
132
 
data/lib/rage/setup.rb CHANGED
@@ -2,5 +2,6 @@ Iodine.patch_rack
2
2
 
3
3
  project_root = Pathname.new(".").expand_path
4
4
 
5
+ require_relative "#{project_root}/config/environments/#{Rage.env}"
5
6
  Dir["#{project_root}/app/**/*.rb"].each { |path| require_relative path }
6
7
  require_relative "#{project_root}/config/routes"
@@ -1,4 +1,6 @@
1
1
  require "bundler/setup"
2
- Bundler.require(:default)
2
+
3
+ require "rage"
4
+ Bundler.require(*Rage.groups)
3
5
 
4
6
  require "rage/setup"
@@ -0,0 +1,7 @@
1
+ Rage.configure do |config|
2
+ # Specify the number of server processes to run. Defaults to number of CPU cores.
3
+ config.workers_count = 1
4
+
5
+ # Specify the port the server will listen on.
6
+ config.port = 3000
7
+ end
@@ -0,0 +1,7 @@
1
+ Rage.configure do |config|
2
+ # Specify the number of server processes to run. Defaults to number of CPU cores.
3
+ # config.workers_count = ENV.fetch("WEB_CONCURRENCY", 1)
4
+
5
+ # Specify the port the server will listen on.
6
+ config.port = 3000
7
+ end
@@ -0,0 +1,7 @@
1
+ Rage.configure do |config|
2
+ # Specify the number of server processes to run. Defaults to number of CPU cores.
3
+ config.workers_count = 1
4
+
5
+ # Specify the port the server will listen on.
6
+ config.port = 3000
7
+ end
data/lib/rage/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rage
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/rage-rb.rb CHANGED
@@ -17,6 +17,22 @@ module Rage
17
17
  @__router ||= Rage::Router::Backend.new
18
18
  end
19
19
 
20
+ def self.config
21
+ @config ||= Rage::Configuration.new
22
+ end
23
+
24
+ def self.configure
25
+ yield(config)
26
+ end
27
+
28
+ def self.env
29
+ @__env ||= ENV["RAGE_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
30
+ end
31
+
32
+ def self.groups
33
+ [:default, Rage.env.to_sym]
34
+ end
35
+
20
36
  module Router
21
37
  module Strategies
22
38
  end
@@ -29,6 +45,7 @@ end
29
45
  require_relative "rage/application"
30
46
  require_relative "rage/fiber"
31
47
  require_relative "rage/fiber_scheduler"
48
+ require_relative "rage/configuration"
32
49
 
33
50
  require_relative "rage/router/strategies/host"
34
51
  require_relative "rage/router/backend"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rage-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Samoilov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-21 00:00:00.000000000 Z
11
+ date: 2023-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -61,6 +61,7 @@ extensions: []
61
61
  extra_rdoc_files: []
62
62
  files:
63
63
  - ".rspec"
64
+ - ".yardopts"
64
65
  - CHANGELOG.md
65
66
  - CODE_OF_CONDUCT.md
66
67
  - Gemfile
@@ -72,6 +73,7 @@ files:
72
73
  - lib/rage.rb
73
74
  - lib/rage/application.rb
74
75
  - lib/rage/cli.rb
76
+ - lib/rage/configuration.rb
75
77
  - lib/rage/controller/api.rb
76
78
  - lib/rage/fiber.rb
77
79
  - lib/rage/fiber_scheduler.rb
@@ -86,6 +88,9 @@ files:
86
88
  - lib/rage/templates/Gemfile
87
89
  - lib/rage/templates/app-controllers-application_controller.rb
88
90
  - lib/rage/templates/config-application.rb
91
+ - lib/rage/templates/config-environments-development.rb
92
+ - lib/rage/templates/config-environments-production.rb
93
+ - lib/rage/templates/config-environments-test.rb
89
94
  - lib/rage/templates/config-routes.rb
90
95
  - lib/rage/templates/config.ru
91
96
  - lib/rage/templates/lib-.keep