kirei 0.2.0 → 0.3.0

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: 8a37402ff7c216a16e3cc2a279d477e91d7ed8c40fd58169a8c598e47efd90d7
4
- data.tar.gz: 1fd249f44b20dc1dfba0ace6ea52f6de6f67ee47e4c2a7f511438ceafba4985f
3
+ metadata.gz: fb100f4fb7ab34f19caf8d428e373ac0e94230b126037147108b4826ca08b0c5
4
+ data.tar.gz: 00efe991393cecf639ffe87ac5407fcf232d068940f55da8cc1696c08bbf75e6
5
5
  SHA512:
6
- metadata.gz: d6d29c323e8b3438a0c9775f2b92312182a709443aa04843145bfed76226d71e2d78081361b178dc6293dfefa75571d525981b5ed84fee2664c93a880eb5f6fa
7
- data.tar.gz: 6b33ff256a0a199333d789c81ff70f4dca49225069f0c854fe0f4cfec4c6a68c918a6966b92c93ef76aebd14117029c5f3e7ad36d65d7d74033c5d7a0f905ab4
6
+ metadata.gz: bee31a71691f31363a7ffc4855c2ef9886f1ce3e545aafa5409a8064e7de79ec78670172b6c7e61348fb5b21f61779e080818e25077b9c300ec4f612dda83e28
7
+ data.tar.gz: 1d4511cbd01bc7f2eb5f03bd3032079232650b30226ca510dce48a07ee4fc7814e0e6e9ac25ba5df1cd14a11ee957a508d2cbededfbf29ee7806b1840d3455b9
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Kirei
2
2
 
3
- Kirei is a strictly typed Ruby micro/REST-framework for building scalable and performant APIs. It is built from the ground up to be clean and easy to use. Kirei is based on [Sequel](https://github.com/jeremyevans/sequel) as an ORM, [Sorbet](https://github.com/sorbet/sorbet) for typing, and [Sinatra](https://github.com/sinatra/sinatra) for routing. It strives to have zero magic and to be as explicit as possible.
3
+ Kirei is a strictly typed Ruby micro/REST-framework for building scalable and performant APIs. It is built from the ground up to be clean and easy to use. Kirei is based on [Sequel](https://github.com/jeremyevans/sequel) as an ORM, [Sorbet](https://github.com/sorbet/sorbet) for typing, and [Rack](https://github.com/rack/rack) as web server interface. It strives to have zero magic and to be as explicit as possible.
4
4
 
5
5
  Kirei's main advantages over other frameworks are its strict typing, low memory footprint, and build-in high-performance logging and metric-tracking toolkits. It is opiniated in terms of tooling, allowing you to focus on your core-business. It is a great choice for building APIs that need to scale.
6
6
 
@@ -53,12 +53,12 @@ Find a test app in the [spec/test_app](spec/test_app) directory. It is a fully f
53
53
 
54
54
  #### Models
55
55
 
56
- All models must inherit from `T::Struct` and include `Kirei::BaseModel`. They must implement `id` which must hold the primary key of the table. The primary key must be named `id` and be of type `T.any(String, Integer)`.
56
+ All models must inherit from `T::Struct` and include `Kirei::Model`. They must implement `id` which must hold the primary key of the table. The primary key must be named `id` and be of type `T.any(String, Integer)`.
57
57
 
58
58
  ```ruby
59
59
  class User < T::Struct
60
60
  extend T::Sig
61
- include Kirei::BaseModel
61
+ include Kirei::Model
62
62
 
63
63
  const :id, T.any(String, Integer)
64
64
  const :name, String
@@ -76,12 +76,21 @@ user.name # => 'John'
76
76
  updated_user.name # => 'Johnny'
77
77
  ```
78
78
 
79
+ Delete keeps the original object intact. Returns `true` if the record was deleted. Calling delete multiple times will return `false` after the first (successful) call.
80
+
81
+ ```ruby
82
+ success = user.delete # => T::Boolean
83
+
84
+ # or delete by any query:
85
+ User.db.where('...').delete # => Integer, number of deleted records
86
+ ```
87
+
79
88
  To build more complex queries, Sequel can be used directly:
80
89
 
81
90
  ```ruby
82
91
  query = User.db.where({ name: 'John' })
83
- query = query.where('...')
84
- query = query.limit(10) # query is a Sequel::Dataset, chain as you like
92
+ query = query.where('...') # "query" is a 'Sequel::Dataset' that you can chain as you like
93
+ query = query.limit(10)
85
94
 
86
95
  users = User.resolve(query) # T::Array[User]
87
96
  first_user = User.resolve_first(query) # T.nilable(User)
@@ -121,11 +130,15 @@ bundle exec rake db:drop
121
130
  # apply all pending migrations
122
131
  bundle exec rake db:migrate
123
132
 
133
+ # annotate the models with the schema
134
+ # this runs automatically after each migration
135
+ bundle exec rake db:annotate
136
+
124
137
  # roll back the last n migration
125
138
  STEPS=1 bundle exec rake db:rollback
126
139
 
127
140
  # run db/seeds.rb to seed the database
128
- bundle exec rake db:migrate
141
+ bundle exec rake db:seed
129
142
 
130
143
  # scaffold a new migration file
131
144
  bundle exec rake 'db:migration[CreateAirports]'
@@ -138,21 +151,24 @@ Define routes anywhere in your app; by convention, they are defined in `config/r
138
151
  ```ruby
139
152
  # config/routes.rb
140
153
 
141
- Kirei::Router.add_routes([
142
- Kirei::Router::Route.new(
143
- verb: "GET",
144
- path: "/livez",
145
- controller: Controllers::Health,
146
- action: "livez",
147
- ),
148
-
149
- Kirei::Router::Route.new(
150
- verb: "GET",
151
- path: "/airports",
152
- controller: Controllers::Airports,
153
- action: "index",
154
- ),
155
- ])
154
+ module Kirei::Routing
155
+ Router.add_routes(
156
+ [
157
+ Router::Route.new(
158
+ verb: Router::Verb::GET,
159
+ path: "/livez",
160
+ controller: Controllers::Health,
161
+ action: "livez",
162
+ ),
163
+ Router::Route.new(
164
+ verb: Router::Verb::GET,
165
+ path: "/airports",
166
+ controller: Controllers::Airports,
167
+ action: "index",
168
+ ),
169
+ ],
170
+ )
171
+ end
156
172
  ```
157
173
 
158
174
  #### Controllers
@@ -161,12 +177,12 @@ Controllers can be defined anywhere; by convention, they are defined in the `app
161
177
 
162
178
  ```ruby
163
179
  module Controllers
164
- class Airports < Kirei::BaseController
180
+ class Airports < Kirei::Controller
165
181
  extend T::Sig
166
182
 
167
- sig { returns(Kirei::Middleware::RackResponseType) }
183
+ sig { returns(T.anything) }
168
184
  def index
169
- airports = Airport.all
185
+ airports = Airport.all # T::Array[Airport]
170
186
 
171
187
  # or use a serializer
172
188
  data = Oj.dump(airports.map(&:serialize))
data/bin/kirei CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- require_relative '../lib/cli'
2
+ require_relative "../lib/cli"
3
3
 
4
4
  Cli::Commands::Start.call(ARGV)
data/kirei.gemspec CHANGED
@@ -13,11 +13,11 @@ Gem::Specification.new do |spec|
13
13
  "oss@dbl.works",
14
14
  ]
15
15
 
16
- spec.summary = "Kirei is a strictly typed Ruby micro/REST-framework for building scaleable and performant microservices." # rubocop:disable Layout/LineLength
16
+ spec.summary = "Kirei is a typed Ruby micro/REST-framework for building scalable and performant microservices."
17
17
  spec.description = <<~TXT
18
- Kirei is a strictly typed Ruby micro/REST-framework for building scaleable and performant microservices.
18
+ Kirei is a Ruby micro/REST-framework for building scalable and performant microservices.
19
19
  It is built from the ground up to be clean and easy to use.
20
- Kirei is based on Sequel as an ORM, Sorbet for typing, and Sinatra for routing.
20
+ It is a Rack app, and uses Sorbet for typing, Sequel as an ORM, Zeitwerk for autoloading, and Puma as a web server.
21
21
  It strives to have zero magic and to be as explicit as possible.
22
22
  TXT
23
23
  spec.homepage = "https://github.com/swiknaba/kirei"
@@ -47,6 +47,7 @@ Gem::Specification.new do |spec|
47
47
  spec.add_dependency "oj", "~> 3.0"
48
48
  spec.add_dependency "sorbet-runtime", "~> 0.5"
49
49
  spec.add_dependency "tzinfo-data", "~> 1.0" # for containerized environments, e.g. on AWS ECS
50
+ spec.add_dependency "zeitwerk", "~> 2.5"
50
51
 
51
52
  # Web server & routing
52
53
  spec.add_dependency "puma", "~> 6.0"
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  module Cli
4
4
  module Commands
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  require "fileutils"
4
4
 
@@ -13,7 +13,8 @@ module Cli
13
13
  Files::DbRake.call(app_name)
14
14
  Files::Irbrc.call
15
15
  Files::Rakefile.call
16
- Files::Routes.call
16
+ Files::Routes.call(app_name)
17
+ Files::SorbetConfig.call
17
18
 
18
19
  Kirei::Logger.logger.info(
19
20
  "Kirei app '#{app_name}' scaffolded successfully!",
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  module Cli
4
4
  module Commands
@@ -29,15 +29,21 @@ module Cli
29
29
  Dir[File.join(__dir__, "config/initializers", "*.rb")].each { require(_1) }
30
30
 
31
31
  # Fourth: load all application code
32
- Dir[File.join(__dir__, "app/**/*", "*.rb")].each { require(_1) }
32
+ loader = Zeitwerk::Loader.new
33
+ loader.tag = File.basename(__FILE__, ".rb")
34
+ loader.push_dir("#{File.dirname(__FILE__)}/app")
35
+ loader.push_dir("#{File.dirname(__FILE__)}/app/models") # make models a root namespace so we don't infer a `Models::` module
36
+ loader.setup
33
37
 
34
38
  # Fifth: load configs
35
39
  Dir[File.join(__dir__, "config", "*.rb")].each { require(_1) }
36
40
 
37
- class #{app_name} < Kirei::AppBase
41
+ class #{app_name} < Kirei::App
38
42
  # Kirei configuration
39
43
  config.app_name = "#{snake_case_app_name}"
40
44
  end
45
+
46
+ loader.eager_load
41
47
  RUBY
42
48
  end
43
49
  end
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  module Cli
4
4
  module Commands
@@ -21,6 +21,7 @@ module Cli
21
21
  #
22
22
  # CREATE DATABASE #{db_name}_${environment};
23
23
 
24
+ require 'zeitwerk/inflector'
24
25
  require_relative "../../app"
25
26
 
26
27
  namespace :db do
@@ -30,7 +31,7 @@ module Cli
30
31
  envs = ENV.key?("RACK_ENV") ? [ENV.fetch("RACK_ENV")] : %w[development test]
31
32
  envs.each do |env|
32
33
  ENV["RACK_ENV"] = env
33
- db_name = "#{db_name}_#{env}"
34
+ db_name = "#{db_name}_\#{env}"
34
35
  puts("Creating database \#{db_name}...")
35
36
 
36
37
  reset_memoized_class_level_instance_vars(#{app_name})
@@ -84,6 +85,8 @@ module Cli
84
85
  current_version = db[:schema_migrations].order(:filename).last[:filename].to_i
85
86
  puts "Migrated \#{db_name} to version \#{current_version}!"
86
87
  end
88
+
89
+ Rake::Task["db:annotate"].invoke
87
90
  end
88
91
 
89
92
  desc "Rollback the last migration"
@@ -128,7 +131,7 @@ module Cli
128
131
  migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S")
129
132
 
130
133
  # Sanitize and format the migration name
131
- formatted_name = args[:name].to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
134
+ formatted_name = args[:name].to_s.gsub(/([a-z])([A-Z])/, '\\1_\\2').downcase
132
135
 
133
136
  # Combine them to create the filename
134
137
  filename = "\#{migration_number}_\#{formatted_name}.rb"
@@ -136,7 +139,7 @@ module Cli
136
139
 
137
140
  # Define the content of the migration file
138
141
  content = <<~MIGRATION
139
- # typed: strict
142
+ # typed: false
140
143
  # frozen_string_literal: true
141
144
 
142
145
  Sequel.migration do
@@ -155,6 +158,38 @@ module Cli
155
158
 
156
159
  puts "Generated migration: db/migrate/\#{filename}"
157
160
  end
161
+
162
+ desc "Write the table schema to each model file, or a single file if filename (without extension) is provided"
163
+ task :annotate, [:model_file_name] do |_t, args|
164
+ require "fileutils"
165
+
166
+ db = #{app_name}.raw_db_connection
167
+ model_file_name = args[:model_file_name]&.to_s
168
+
169
+ models_dir = #{app_name}.root
170
+
171
+ Dir.glob("app/models/*.rb").each do |model_file|
172
+ next if !model_file_name.nil? && model_file == model_file_name
173
+
174
+ model_path = File.expand_path(model_file, models_dir)
175
+ model_name = Zeitwerk::Inflector.new.camelize(File.basename(model_file, ".rb"), model_path)
176
+ model_klass = Object.const_get(model_name)
177
+ table_name = model_klass.table_name
178
+ schema = db.schema(table_name)
179
+
180
+ schema_comments = format_schema_comments(table_name, schema)
181
+
182
+ file_contents = File.read(model_path)
183
+
184
+ # Remove existing schema info comments if present
185
+ updated_contents = file_contents.sub(/# == Schema Info\\n(.*?)(\\n#\\n)?\\n(?=\\s*class)/m, "")
186
+
187
+ # Insert the new schema comments before the class definition
188
+ modified_contents = updated_contents.sub(/(\A|\\n)(class \#{model_name})/m, "\\\\1\#{schema_comments}\\n\\n\\\\2")
189
+
190
+ File.write(model_path, modified_contents)
191
+ end
192
+ end
158
193
  end
159
194
 
160
195
  def reset_memoized_class_level_instance_vars(app)
@@ -167,6 +202,19 @@ module Cli
167
202
  end
168
203
  end
169
204
 
205
+ def format_schema_comments(table_name, schema)
206
+ lines = ["# == Schema Info", "#", "# Table name: \#{table_name}", "#"]
207
+ schema.each do |column|
208
+ name, info = column
209
+ type = "\#{info[:db_type]}(\#{info[:max_length]})" if info[:max_length]
210
+ type ||= info[:db_type]
211
+ null = info[:allow_null] ? 'null' : 'not null'
212
+ primary_key = info[:primary_key] ? ', primary key' : ''
213
+ lines << "# \#{name.to_s.ljust(20)}:\#{type} \#{null}\#{primary_key}"
214
+ end
215
+ lines.join("\\n") + "\\n#"
216
+ end
217
+
170
218
  RUBY
171
219
  end
172
220
  end
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  module Cli
4
4
  module Commands
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  module Cli
4
4
  module Commands
@@ -1,27 +1,64 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  module Cli
4
4
  module Commands
5
5
  module NewApp
6
6
  module Files
7
7
  class Routes
8
- def self.call
9
- File.write("config/routes.rb", content)
8
+ def self.call(app_name)
9
+ File.write("config/routes.rb", router)
10
+ File.write("app/controllers/base.rb", base_controller)
11
+ File.write("app/controllers/health.rb", health_controller(app_name))
10
12
  end
11
13
 
12
- def self.content
14
+ def self.router
13
15
  <<~RUBY
14
16
  # typed: strict
15
17
  # frozen_string_literal: true
16
18
 
17
- Kirei::Router.add_routes([
18
- # Kirei::Router::Route.new(
19
- # verb: "GET",
20
- # path: "/livez",
21
- # controller: Controllers::HealthController,
22
- # action: "livez",
23
- # )
24
- ])
19
+ module Kirei::Routing
20
+ Router.add_routes(
21
+ [
22
+ Router::Route.new(
23
+ verb: Router::Verb::GET,
24
+ path: "/livez",
25
+ controller: Controllers::Health,
26
+ action: "livez",
27
+ ),
28
+ ],
29
+ )
30
+ end
31
+ RUBY
32
+ end
33
+
34
+ def self.base_controller
35
+ <<~RUBY
36
+ # typed: strict
37
+ # frozen_string_literal: true
38
+
39
+ module Controllers
40
+ class Base < Kirei::Controller
41
+ extend T::Sig
42
+ end
43
+ end
44
+ RUBY
45
+ end
46
+
47
+ def self.health_controller(app_name)
48
+ <<~RUBY
49
+ # typed: strict
50
+ # frozen_string_literal: true
51
+
52
+ module Controllers
53
+ class Health < Base
54
+ sig { returns(T.anything) }
55
+ def livez
56
+ #{app_name}.config.logger.info("Health check")
57
+ #{app_name}.config.logger.info(params.inspect)
58
+ render(#{app_name}.version, status: 200)
59
+ end
60
+ end
61
+ end
25
62
  RUBY
26
63
  end
27
64
  end
@@ -0,0 +1,25 @@
1
+ # typed: true
2
+
3
+ module Cli
4
+ module Commands
5
+ module NewApp
6
+ module Files
7
+ class SorbetConfig
8
+ def self.call
9
+ File.write("sorbet/config", content)
10
+ end
11
+
12
+ def self.content
13
+ <<~TXT
14
+ --dir
15
+ .
16
+ --ignore=vendor/
17
+ --ignore=spec/
18
+
19
+ TXT
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/kirei/app.rb CHANGED
@@ -1,72 +1,89 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative("middleware")
4
+ module Kirei
5
+ class App < Routing::Base
6
+ class << self
7
+ extend T::Sig
5
8
 
6
- # rubocop:disable Metrics/AbcSize, Layout/LineLength
9
+ #
10
+ # convenience method since "Kirei.configuration" must be nilable since it is nil
11
+ # at the beginning of initilization of the app
12
+ #
13
+ sig { returns(Kirei::Config) }
14
+ def config
15
+ T.must(Kirei.configuration)
16
+ end
7
17
 
8
- module Kirei
9
- class App
10
- include Middleware
11
- extend T::Sig
18
+ sig { returns(Pathname) }
19
+ def root
20
+ defined?(::APP_ROOT) ? Pathname.new(::APP_ROOT) : Pathname.new(Dir.pwd)
21
+ end
12
22
 
13
- sig { params(params: T::Hash[String, T.untyped]).void }
14
- def initialize(params: {})
15
- @router = T.let(Router.instance, Router)
16
- @params = T.let(params, T::Hash[String, T.untyped])
17
- end
23
+ #
24
+ # Returns the version of the app. It checks in the following order:
25
+ # * ENV["APP_VERSION"]
26
+ # * ENV["GIT_SHA"]
27
+ # * `git rev-parse --short HEAD`
28
+ #
29
+ sig { returns(String) }
30
+ def version
31
+ @version = T.let(@version, T.nilable(String))
32
+ @version ||= ENV.fetch("APP_VERSION", nil)
33
+ @version ||= ENV.fetch("GIT_SHA", nil)
34
+ @version ||= T.must(
35
+ `command -v git && git rev-parse --short HEAD`.to_s.split("\n").last,
36
+ ).freeze # localhost
37
+ end
18
38
 
19
- sig { returns(T::Hash[String, T.untyped]) }
20
- attr_reader :params
39
+ #
40
+ # Returns ENV["RACK_ENV"] or "development" if it is not set
41
+ #
42
+ sig { returns(String) }
43
+ def environment
44
+ ENV.fetch("RACK_ENV", "development")
45
+ end
21
46
 
22
- sig { params(env: RackEnvType).returns(RackResponseType) }
23
- def call(env)
24
- http_verb = T.cast(env.fetch("REQUEST_METHOD"), String)
25
- req_path = T.cast(env.fetch("REQUEST_PATH"), String)
26
- # reject requests from unexpected hosts -> allow configuring allowed hosts in a `cors.rb` file
27
- # ( offer a scaffold for this file )
28
- # -> use https://github.com/cyu/rack-cors
47
+ #
48
+ # Returns the name of the database based on the app name and the environment,
49
+ # e.g. "myapp_development"
50
+ #
51
+ sig { returns(String) }
52
+ def default_db_name
53
+ @default_db_name ||= T.let("#{config.app_name}_#{environment}".freeze, T.nilable(String))
54
+ end
29
55
 
30
- route = Router.instance.get(http_verb, req_path)
31
- return [404, {}, ["Not Found"]] if route.nil?
56
+ #
57
+ # Returns the database URL based on the DATABASE_URL environment variable or
58
+ # a default value based on the default_db_name
59
+ #
60
+ sig { returns(String) }
61
+ def default_db_url
62
+ @default_db_url ||= T.let(
63
+ ENV.fetch("DATABASE_URL", "postgresql://localhost:5432/#{default_db_name}"),
64
+ T.nilable(String),
65
+ )
66
+ end
32
67
 
33
- params = if route.verb == "GET"
34
- query = T.cast(env.fetch("QUERY_STRING"), String)
35
- query.split("&").to_h do |p|
36
- k, v = p.split("=")
37
- k = T.cast(k, String)
38
- [k, v]
68
+ sig { returns(Sequel::Database) }
69
+ def raw_db_connection
70
+ @raw_db_connection = T.let(@raw_db_connection, T.nilable(Sequel::Database))
71
+ return @raw_db_connection unless @raw_db_connection.nil?
72
+
73
+ # calling "Sequel.connect" creates a new connection
74
+ @raw_db_connection = Sequel.connect(App.config.db_url || default_db_url)
75
+
76
+ config.db_extensions.each do |ext|
77
+ T.cast(@raw_db_connection, Sequel::Database).extension(ext)
39
78
  end
40
- else
41
- # TODO: based on content-type, parse the body differently
42
- # build-in support for JSON & XML
43
- body = T.cast(env.fetch("rack.input"), T.any(IO, StringIO))
44
- res = Oj.load(body.read, Kirei::OJ_OPTIONS)
45
- body.rewind # TODO: maybe don't rewind if we don't need to?
46
- T.cast(res, T::Hash[String, T.untyped])
47
- end
48
79
 
49
- instance = route.controller.new(params: params)
50
- instance.public_send(route.action)
51
- end
80
+ if config.db_extensions.include?(:pg_json)
81
+ # https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb#L8
82
+ @raw_db_connection.wrap_json_primitives = true
83
+ end
52
84
 
53
- sig do
54
- params(
55
- status: Integer,
56
- body: String,
57
- headers: T::Hash[String, String],
58
- ).returns(RackResponseType)
59
- end
60
- def render(status:, body:, headers: {})
61
- # merge default headers
62
- # support a "type" to set content-type header? (or default to json, and users must set the header themselves for other types?)
63
- [
64
- status,
65
- headers,
66
- [body],
67
- ]
85
+ @raw_db_connection
86
+ end
68
87
  end
69
88
  end
70
89
  end
71
-
72
- # rubocop:enable Metrics/AbcSize, Layout/LineLength
@@ -0,0 +1,44 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ class Controller < Routing::Base
6
+ class << self
7
+ extend T::Sig
8
+
9
+ sig { returns(Routing::NilableHooksType) }
10
+ attr_reader :before_hooks, :after_hooks
11
+ end
12
+
13
+ extend T::Sig
14
+
15
+ #
16
+ # Statements to be executed before every action.
17
+ #
18
+ # In development mode, Rack Reloader might reload this file causing
19
+ # the before hooks to be executed multiple times.
20
+ #
21
+ sig do
22
+ params(
23
+ block: T.nilable(T.proc.void),
24
+ ).void
25
+ end
26
+ def self.before(&block)
27
+ @before_hooks ||= T.let(Set.new, Routing::NilableHooksType)
28
+ @before_hooks.add(block) if block
29
+ end
30
+
31
+ #
32
+ # Statements to be executed after every action.
33
+ #
34
+ sig do
35
+ params(
36
+ block: T.nilable(T.proc.void),
37
+ ).void
38
+ end
39
+ def self.after(&block)
40
+ @after_hooks ||= T.let(Set.new, Routing::NilableHooksType)
41
+ @after_hooks.add(block) if block
42
+ end
43
+ end
44
+ end