kirei 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 }