kirei 0.2.1 → 0.4.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +74 -25
  3. data/bin/kirei +1 -1
  4. data/kirei.gemspec +5 -3
  5. data/lib/cli/commands/new_app/base_directories.rb +1 -1
  6. data/lib/cli/commands/new_app/execute.rb +3 -3
  7. data/lib/cli/commands/new_app/files/app.rb +16 -3
  8. data/lib/cli/commands/new_app/files/config_ru.rb +1 -1
  9. data/lib/cli/commands/new_app/files/db_rake.rb +50 -2
  10. data/lib/cli/commands/new_app/files/irbrc.rb +1 -1
  11. data/lib/cli/commands/new_app/files/rakefile.rb +1 -1
  12. data/lib/cli/commands/new_app/files/routes.rb +49 -12
  13. data/lib/cli/commands/new_app/files/sorbet_config.rb +1 -1
  14. data/lib/cli/commands/start.rb +1 -1
  15. data/lib/kirei/app.rb +76 -56
  16. data/lib/kirei/config.rb +4 -1
  17. data/lib/kirei/controller.rb +44 -0
  18. data/lib/kirei/errors/json_api_error.rb +25 -0
  19. data/lib/kirei/errors/json_api_error_source.rb +12 -0
  20. data/lib/kirei/logging/level.rb +33 -0
  21. data/lib/kirei/logging/logger.rb +198 -0
  22. data/lib/kirei/logging/metric.rb +40 -0
  23. data/lib/kirei/model/base_class_interface.rb +55 -0
  24. data/lib/kirei/{base_model.rb → model/class_methods.rb} +42 -108
  25. data/lib/kirei/model/human_id_generator.rb +40 -0
  26. data/lib/kirei/model.rb +52 -0
  27. data/lib/kirei/routing/base.rb +187 -0
  28. data/lib/kirei/routing/nilable_hooks_type.rb +10 -0
  29. data/lib/kirei/{middleware.rb → routing/rack_env_type.rb} +1 -10
  30. data/lib/kirei/routing/rack_response_type.rb +15 -0
  31. data/lib/kirei/routing/route.rb +13 -0
  32. data/lib/kirei/routing/router.rb +56 -0
  33. data/lib/kirei/routing/verb.rb +37 -0
  34. data/lib/kirei/services/result.rb +53 -0
  35. data/lib/kirei/services/runner.rb +47 -0
  36. data/lib/kirei/version.rb +1 -1
  37. data/lib/kirei.rb +31 -3
  38. data/sorbet/rbi/shims/base_model.rbi +1 -1
  39. data/sorbet/rbi/shims/ruby.rbi +15 -0
  40. metadata +55 -14
  41. data/lib/boot.rb +0 -23
  42. data/lib/kirei/app_base.rb +0 -72
  43. data/lib/kirei/base_controller.rb +0 -16
  44. data/lib/kirei/logger.rb +0 -196
  45. data/lib/kirei/router.rb +0 -61
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7eaf9be851bf8985a4c169c4ed79c3bc3cc067b247b0cc50e1f742447e019c3d
4
- data.tar.gz: 47816fbc66608113d804e9efcbc7267cd3deea522edcdc8c5920d7ac365cd1b6
3
+ metadata.gz: 788221745d889864fe2ec7075cce289f6d44361dece7711fbe91ac24c16f693b
4
+ data.tar.gz: e03e3cf0c4c6db69bc0106107ccb002b8f472ba077ef457d88a84b92b188371f
5
5
  SHA512:
6
- metadata.gz: 2f66ca601807ae0f8528f59b0303812d6bf2b644a910284f4caedbe413c5865d3634c12a6a8e30c1a3ae85ff44f7f80fd8b69c48a977e4cec98260b30c09d56e
7
- data.tar.gz: 8ec07fe4d0a69c0918026a966e96eb1257767e187c9bde5edc9d5be9f6796269bccc5f989c399df8e211965f1014c51a24739c638a78cee971b4dddb9abdf821
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 [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.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.db.where({ name: 'John' })
83
- query = query.where('...')
84
- query = query.limit(10) # query is a Sequel::Dataset, chain as you like
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: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
+ 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::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
+ 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
@@ -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"
@@ -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: 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,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: false
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
- Dir[File.join(__dir__, "app/**/*", "*.rb")].each { require(_1) }
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::AppBase
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
@@ -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"
@@ -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
+ 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
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  module Cli
4
4
  module Commands
@@ -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
- require_relative("middleware")
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
- # rubocop:disable Metrics/AbcSize, Layout/LineLength
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
- module Kirei
9
- class App
10
- include Middleware
11
- extend T::Sig
21
+ sig { returns(Pathname) }
22
+ def root
23
+ defined?(::APP_ROOT) ? Pathname.new(::APP_ROOT) : Pathname.new(Dir.pwd)
24
+ end
12
25
 
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
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
- sig { returns(T::Hash[String, T.untyped]) }
20
- attr_reader :params
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
- 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
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
- route = Router.instance.get(http_verb, req_path)
31
- return [404, {}, ["Not Found"]] if route.nil?
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
- 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]
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
- instance = route.controller.new(params: params)
50
- instance.public_send(route.action)
51
- end
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
- 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
- ]
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[Symbol, String], default: {}
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 }