kirei 0.2.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +74 -25
- data/bin/kirei +1 -1
- data/kirei.gemspec +5 -3
- data/lib/cli/commands/new_app/base_directories.rb +1 -1
- data/lib/cli/commands/new_app/execute.rb +3 -3
- data/lib/cli/commands/new_app/files/app.rb +16 -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/cli/commands/start.rb +1 -1
- data/lib/kirei/app.rb +76 -56
- data/lib/kirei/config.rb +4 -1
- data/lib/kirei/controller.rb +44 -0
- data/lib/kirei/errors/json_api_error.rb +25 -0
- data/lib/kirei/errors/json_api_error_source.rb +12 -0
- data/lib/kirei/logging/level.rb +33 -0
- data/lib/kirei/logging/logger.rb +198 -0
- data/lib/kirei/logging/metric.rb +40 -0
- data/lib/kirei/model/base_class_interface.rb +55 -0
- data/lib/kirei/{base_model.rb → model/class_methods.rb} +42 -108
- data/lib/kirei/model/human_id_generator.rb +40 -0
- data/lib/kirei/model.rb +52 -0
- data/lib/kirei/routing/base.rb +187 -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/route.rb +13 -0
- data/lib/kirei/routing/router.rb +56 -0
- data/lib/kirei/routing/verb.rb +37 -0
- data/lib/kirei/services/result.rb +53 -0
- data/lib/kirei/services/runner.rb +47 -0
- data/lib/kirei/version.rb +1 -1
- data/lib/kirei.rb +31 -3
- data/sorbet/rbi/shims/base_model.rbi +1 -1
- data/sorbet/rbi/shims/ruby.rbi +15 -0
- metadata +55 -14
- 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/logger.rb +0 -196
- 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: 788221745d889864fe2ec7075cce289f6d44361dece7711fbe91ac24c16f693b
|
4
|
+
data.tar.gz: e03e3cf0c4c6db69bc0106107ccb002b8f472ba077ef457d88a84b92b188371f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30bef2a1458e1aeeebd774d577df1dc231b0cfa46fb7483b076b69ac05de1529527442599dca18bb278fec42e1a17888eee849feb1b3f81dd502fd92284dd879
|
7
|
+
data.tar.gz: 181df5735f94c222fc29e9bb3c18aa33340995925a324a18a0bb28e1001eaeaee24f8c7442cc5852adf31ae7e5e7a707743441e1c64a585e27039e7b50ba96da
|
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.query.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
|
-
query = User.
|
83
|
-
query = query.where('...')
|
84
|
-
query = query.limit(10)
|
91
|
+
query = User.query.where({ name: 'John' })
|
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
|
+
Route.new(
|
158
|
+
verb: Verb::GET,
|
159
|
+
path: "/livez",
|
160
|
+
controller: Controllers::Health,
|
161
|
+
action: "livez",
|
162
|
+
),
|
163
|
+
Route.new(
|
164
|
+
verb: 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,16 @@ 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
|
-
|
185
|
+
search = T.let(params.fetch("q", nil), T.nilable(String))
|
186
|
+
|
187
|
+
airports = Kirei::Services::Runner.call("Airports::Filter") do
|
188
|
+
Airports::Filter.call(search) # T::Array[Airport]
|
189
|
+
end
|
170
190
|
|
171
191
|
# or use a serializer
|
172
192
|
data = Oj.dump(airports.map(&:serialize))
|
@@ -177,6 +197,35 @@ module Controllers
|
|
177
197
|
end
|
178
198
|
```
|
179
199
|
|
200
|
+
Services can be PORO. You can wrap an execution in `Kirei::Services::Runner` which will emit a standardized logline and track its execution time.
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
module Airports
|
204
|
+
class Filter
|
205
|
+
extend T::Sig
|
206
|
+
|
207
|
+
sig do
|
208
|
+
params(
|
209
|
+
search: T.nilable(String),
|
210
|
+
).returns(T::Array[Airport])
|
211
|
+
end
|
212
|
+
def self.call(search)
|
213
|
+
return Airport.all if search.nil?
|
214
|
+
|
215
|
+
#
|
216
|
+
# SELECT *
|
217
|
+
# FROM "airports"
|
218
|
+
# WHERE (("name" ILIKE 'xx%') OR ("id" ILIKE 'xx%'))
|
219
|
+
#
|
220
|
+
query = Airport.query.where(Sequel.ilike(:name, "#{search}%"))
|
221
|
+
query = query.or(Sequel.ilike(:id, "#{search}%"))
|
222
|
+
|
223
|
+
Airport.resolve(query)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
180
229
|
## Contributions
|
181
230
|
|
182
231
|
We welcome contributions from the community. Before starting work on a major feature, please get in touch with us either via email or by opening an issue on GitHub. "Major feature" means anything that changes user-facing features or significant changes to the codebase itself.
|
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"
|
@@ -46,7 +46,9 @@ Gem::Specification.new do |spec|
|
|
46
46
|
# Utilities
|
47
47
|
spec.add_dependency "oj", "~> 3.0"
|
48
48
|
spec.add_dependency "sorbet-runtime", "~> 0.5"
|
49
|
+
spec.add_dependency "statsd-instrument", "~> 3.0"
|
49
50
|
spec.add_dependency "tzinfo-data", "~> 1.0" # for containerized environments, e.g. on AWS ECS
|
51
|
+
spec.add_dependency "zeitwerk", "~> 2.5"
|
50
52
|
|
51
53
|
# Web server & routing
|
52
54
|
spec.add_dependency "puma", "~> 6.0"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: true
|
2
2
|
|
3
3
|
require "fileutils"
|
4
4
|
|
@@ -13,10 +13,10 @@ 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
|
-
Kirei::Logger.logger.info(
|
19
|
+
Kirei::Logging::Logger.logger.info(
|
20
20
|
"Kirei app '#{app_name}' scaffolded successfully!",
|
21
21
|
)
|
22
22
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: true
|
2
2
|
|
3
3
|
module Cli
|
4
4
|
module Commands
|
@@ -29,15 +29,28 @@ 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
|
+
[
|
35
|
+
"/app",
|
36
|
+
"/app/models",
|
37
|
+
"/app/services",
|
38
|
+
].each do |root_namespace|
|
39
|
+
# a root namespace skips the auto-infered module for this folder
|
40
|
+
# so we don't have to write e.g. `Models::` or `Services::`
|
41
|
+
loader.push_dir("\#{File.dirname(__FILE__)}\#{root_namespace}")
|
42
|
+
end
|
43
|
+
loader.setup
|
33
44
|
|
34
45
|
# Fifth: load configs
|
35
46
|
Dir[File.join(__dir__, "config", "*.rb")].each { require(_1) }
|
36
47
|
|
37
|
-
class #{app_name} < Kirei::
|
48
|
+
class #{app_name} < Kirei::App
|
38
49
|
# Kirei configuration
|
39
50
|
config.app_name = "#{snake_case_app_name}"
|
40
51
|
end
|
52
|
+
|
53
|
+
loader.eager_load
|
41
54
|
RUBY
|
42
55
|
end
|
43
56
|
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
|
+
Route.new(
|
23
|
+
verb: 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/cli/commands/start.rb
CHANGED
@@ -13,7 +13,7 @@ module Cli
|
|
13
13
|
app_name = app_name.split("_").map(&:capitalize).join if app_name.include?("_")
|
14
14
|
NewApp::Execute.call(app_name: app_name)
|
15
15
|
else
|
16
|
-
Kirei::Logger.logger.info("Unknown command")
|
16
|
+
Kirei::Logging::Logger.logger.info("Unknown command")
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
data/lib/kirei/app.rb
CHANGED
@@ -1,72 +1,92 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
4
|
+
module Kirei
|
5
|
+
#
|
6
|
+
# This is the entrypoint into the application; it implements the Rack interface.
|
7
|
+
#
|
8
|
+
class App < Routing::Base
|
9
|
+
class << self
|
10
|
+
extend T::Sig
|
5
11
|
|
6
|
-
#
|
12
|
+
#
|
13
|
+
# convenience method since "Kirei.configuration" must be nilable since it is nil
|
14
|
+
# at the beginning of initilization of the app
|
15
|
+
#
|
16
|
+
sig { returns(Kirei::Config) }
|
17
|
+
def config
|
18
|
+
T.must(Kirei.configuration)
|
19
|
+
end
|
7
20
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
21
|
+
sig { returns(Pathname) }
|
22
|
+
def root
|
23
|
+
defined?(::APP_ROOT) ? Pathname.new(::APP_ROOT) : Pathname.new(Dir.pwd)
|
24
|
+
end
|
12
25
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
26
|
+
#
|
27
|
+
# Returns the version of the app. It checks in the following order:
|
28
|
+
# * ENV["APP_VERSION"]
|
29
|
+
# * ENV["GIT_SHA"]
|
30
|
+
# * `git rev-parse --short HEAD`
|
31
|
+
#
|
32
|
+
sig { returns(String) }
|
33
|
+
def version
|
34
|
+
@version = T.let(@version, T.nilable(String))
|
35
|
+
@version ||= ENV.fetch("APP_VERSION", nil)
|
36
|
+
@version ||= ENV.fetch("GIT_SHA", nil)
|
37
|
+
@version ||= T.must(
|
38
|
+
`command -v git && git rev-parse --short HEAD`.to_s.split("\n").last,
|
39
|
+
).freeze # localhost
|
40
|
+
end
|
18
41
|
|
19
|
-
|
20
|
-
|
42
|
+
#
|
43
|
+
# Returns ENV["RACK_ENV"] or "development" if it is not set
|
44
|
+
#
|
45
|
+
sig { returns(String) }
|
46
|
+
def environment
|
47
|
+
ENV.fetch("RACK_ENV", "development")
|
48
|
+
end
|
21
49
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
50
|
+
#
|
51
|
+
# Returns the name of the database based on the app name and the environment,
|
52
|
+
# e.g. "myapp_development"
|
53
|
+
#
|
54
|
+
sig { returns(String) }
|
55
|
+
def default_db_name
|
56
|
+
@default_db_name ||= T.let("#{config.app_name}_#{environment}".freeze, T.nilable(String))
|
57
|
+
end
|
29
58
|
|
30
|
-
|
31
|
-
|
59
|
+
#
|
60
|
+
# Returns the database URL based on the DATABASE_URL environment variable or
|
61
|
+
# a default value based on the default_db_name
|
62
|
+
#
|
63
|
+
sig { returns(String) }
|
64
|
+
def default_db_url
|
65
|
+
@default_db_url ||= T.let(
|
66
|
+
ENV.fetch("DATABASE_URL", "postgresql://localhost:5432/#{default_db_name}"),
|
67
|
+
T.nilable(String),
|
68
|
+
)
|
69
|
+
end
|
32
70
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
71
|
+
sig { returns(Sequel::Database) }
|
72
|
+
def raw_db_connection
|
73
|
+
@raw_db_connection = T.let(@raw_db_connection, T.nilable(Sequel::Database))
|
74
|
+
return @raw_db_connection unless @raw_db_connection.nil?
|
75
|
+
|
76
|
+
# calling "Sequel.connect" creates a new connection
|
77
|
+
@raw_db_connection = Sequel.connect(App.config.db_url || default_db_url)
|
78
|
+
|
79
|
+
config.db_extensions.each do |ext|
|
80
|
+
T.cast(@raw_db_connection, Sequel::Database).extension(ext)
|
39
81
|
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
82
|
|
49
|
-
|
50
|
-
|
51
|
-
|
83
|
+
if config.db_extensions.include?(:pg_json)
|
84
|
+
# https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb#L8
|
85
|
+
@raw_db_connection.wrap_json_primitives = true
|
86
|
+
end
|
52
87
|
|
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
|
-
]
|
88
|
+
@raw_db_connection
|
89
|
+
end
|
68
90
|
end
|
69
91
|
end
|
70
92
|
end
|
71
|
-
|
72
|
-
# rubocop:enable Metrics/AbcSize, Layout/LineLength
|
data/lib/kirei/config.rb
CHANGED
@@ -17,7 +17,10 @@ module Kirei
|
|
17
17
|
|
18
18
|
prop :logger, ::Logger, factory: -> { ::Logger.new($stdout) }
|
19
19
|
prop :log_transformer, T.nilable(T.proc.params(msg: T::Hash[Symbol, T.untyped]).returns(T::Array[String]))
|
20
|
-
prop :log_default_metadata, T::Hash[
|
20
|
+
prop :log_default_metadata, T::Hash[String, T.untyped], default: {}
|
21
|
+
prop :log_level, Kirei::Logging::Level, default: Kirei::Logging::Level::INFO
|
22
|
+
|
23
|
+
prop :metric_default_tags, T::Hash[String, T.untyped], default: {}
|
21
24
|
|
22
25
|
# dup to allow the user to extend the existing list of sensitive keys
|
23
26
|
prop :sensitive_keys, T::Array[Regexp], factory: -> { SENSITIVE_KEYS.dup }
|