kirei 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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