kirei 0.2.1 → 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 +4 -4
- data/README.md +40 -24
- data/bin/kirei +1 -1
- data/kirei.gemspec +4 -3
- data/lib/cli/commands/new_app/base_directories.rb +1 -1
- data/lib/cli/commands/new_app/execute.rb +2 -2
- data/lib/cli/commands/new_app/files/app.rb +9 -3
- data/lib/cli/commands/new_app/files/config_ru.rb +1 -1
- data/lib/cli/commands/new_app/files/db_rake.rb +50 -2
- data/lib/cli/commands/new_app/files/irbrc.rb +1 -1
- data/lib/cli/commands/new_app/files/rakefile.rb +1 -1
- data/lib/cli/commands/new_app/files/routes.rb +49 -12
- data/lib/cli/commands/new_app/files/sorbet_config.rb +1 -1
- data/lib/kirei/app.rb +73 -56
- data/lib/kirei/controller.rb +44 -0
- data/lib/kirei/logger.rb +8 -8
- data/lib/kirei/{base_model.rb → model.rb} +5 -5
- data/lib/kirei/routing/base.rb +156 -0
- data/lib/kirei/routing/nilable_hooks_type.rb +10 -0
- data/lib/kirei/{middleware.rb → routing/rack_env_type.rb} +1 -10
- data/lib/kirei/routing/rack_response_type.rb +15 -0
- data/lib/kirei/routing/router.rb +86 -0
- data/lib/kirei/version.rb +1 -1
- data/lib/kirei.rb +27 -2
- data/sorbet/rbi/shims/base_model.rbi +1 -1
- metadata +28 -13
- data/lib/boot.rb +0 -23
- data/lib/kirei/app_base.rb +0 -72
- data/lib/kirei/base_controller.rb +0 -16
- data/lib/kirei/router.rb +0 -61
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb100f4fb7ab34f19caf8d428e373ac0e94230b126037147108b4826ca08b0c5
|
|
4
|
+
data.tar.gz: 00efe991393cecf639ffe87ac5407fcf232d068940f55da8cc1696c08bbf75e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 [
|
|
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::
|
|
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::
|
|
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)
|
|
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:
|
|
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::
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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::
|
|
180
|
+
class Airports < Kirei::Controller
|
|
165
181
|
extend T::Sig
|
|
166
182
|
|
|
167
|
-
sig { returns(
|
|
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
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
1
|
+
# typed: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ 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
17
|
Files::SorbetConfig.call
|
|
18
18
|
|
|
19
19
|
Kirei::Logger.logger.info(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
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
|
-
|
|
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::
|
|
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
|
|
@@ -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}_
|
|
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])/, '
|
|
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"
|
|
@@ -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,27 +1,64 @@
|
|
|
1
|
-
# typed:
|
|
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",
|
|
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.
|
|
14
|
+
def self.router
|
|
13
15
|
<<~RUBY
|
|
14
16
|
# typed: strict
|
|
15
17
|
# frozen_string_literal: true
|
|
16
18
|
|
|
17
|
-
Kirei::
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
data/lib/kirei/app.rb
CHANGED
|
@@ -1,72 +1,89 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
module Kirei
|
|
5
|
+
class App < Routing::Base
|
|
6
|
+
class << self
|
|
7
|
+
extend T::Sig
|
|
5
8
|
|
|
6
|
-
#
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
sig { returns(Pathname) }
|
|
19
|
+
def root
|
|
20
|
+
defined?(::APP_ROOT) ? Pathname.new(::APP_ROOT) : Pathname.new(Dir.pwd)
|
|
21
|
+
end
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
data/lib/kirei/logger.rb
CHANGED
|
@@ -17,12 +17,12 @@ module Kirei
|
|
|
17
17
|
#
|
|
18
18
|
# You can define a custom log transformer to transform the logline:
|
|
19
19
|
#
|
|
20
|
-
# Kirei::
|
|
20
|
+
# Kirei::App.config.log_transformer = Proc.new { _1 }
|
|
21
21
|
#
|
|
22
|
-
# By default, "meta" is flattened, and sensitive values are masked using see `Kirei::
|
|
22
|
+
# By default, "meta" is flattened, and sensitive values are masked using see `Kirei::App.config.sensitive_keys`.
|
|
23
23
|
# You can also build on top of the provided log transformer:
|
|
24
24
|
#
|
|
25
|
-
# Kirei::
|
|
25
|
+
# Kirei::App.config.log_transformer = Proc.new do |meta|
|
|
26
26
|
# flattened_meta = Kirei::Logger.flatten_hash_and_mask_sensitive_values(meta)
|
|
27
27
|
# # Do something with the flattened meta
|
|
28
28
|
# flattened_meta.map { _1.to_json }
|
|
@@ -85,7 +85,7 @@ module Kirei
|
|
|
85
85
|
).void
|
|
86
86
|
end
|
|
87
87
|
def call(level:, label:, meta: {})
|
|
88
|
-
Kirei::
|
|
88
|
+
Kirei::App.config.log_default_metadata.each_pair do |key, value|
|
|
89
89
|
meta[key] ||= value
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -94,7 +94,7 @@ module Kirei
|
|
|
94
94
|
# Source: https://opentelemetry.io/docs/concepts/semantic-conventions/
|
|
95
95
|
#
|
|
96
96
|
meta[:"service.instance.id"] ||= Thread.current[:request_id]
|
|
97
|
-
meta[:"service.name"] ||= Kirei::
|
|
97
|
+
meta[:"service.name"] ||= Kirei::App.config.app_name
|
|
98
98
|
|
|
99
99
|
# The Ruby logger only accepts one string as the only argument
|
|
100
100
|
@queue << { level: level, label: label, meta: meta }
|
|
@@ -108,12 +108,12 @@ module Kirei
|
|
|
108
108
|
level = log_data.fetch(:level)
|
|
109
109
|
label = log_data.fetch(:label)
|
|
110
110
|
meta = T.let(log_data.fetch(:meta), T::Hash[Symbol, T.untyped])
|
|
111
|
-
meta[:"service.version"] ||= Kirei::
|
|
111
|
+
meta[:"service.version"] ||= Kirei::App.version
|
|
112
112
|
meta[:timestamp] ||= Time.now.utc.iso8601
|
|
113
113
|
meta[:level] ||= level.to_s.upcase
|
|
114
114
|
meta[:label] ||= label
|
|
115
115
|
|
|
116
|
-
log_transformer =
|
|
116
|
+
log_transformer = App.config.log_transformer
|
|
117
117
|
|
|
118
118
|
loglines = if log_transformer
|
|
119
119
|
log_transformer.call(meta)
|
|
@@ -137,7 +137,7 @@ module Kirei
|
|
|
137
137
|
).returns(String)
|
|
138
138
|
end
|
|
139
139
|
def self.mask(k, v)
|
|
140
|
-
return Kirei::Logger::FILTERED if
|
|
140
|
+
return Kirei::Logger::FILTERED if App.config.sensitive_keys.any? { k.match?(_1) }
|
|
141
141
|
|
|
142
142
|
v
|
|
143
143
|
end
|