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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kirei
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ludwig Reinmiedl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-09 00:00:00.000000000 Z
11
+ date: 2024-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: statsd-instrument
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: tzinfo-data
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: zeitwerk
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.5'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.5'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: puma
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -123,9 +151,9 @@ dependencies:
123
151
  - !ruby/object:Gem::Version
124
152
  version: '1.0'
125
153
  description: |
126
- Kirei is a strictly typed Ruby micro/REST-framework for building scaleable and performant microservices.
154
+ Kirei is a Ruby micro/REST-framework for building scalable and performant microservices.
127
155
  It is built from the ground up to be clean and easy to use.
128
- Kirei is based on Sequel as an ORM, Sorbet for typing, and Sinatra for routing.
156
+ It is a Rack app, and uses Sorbet for typing, Sequel as an ORM, Zeitwerk for autoloading, and Puma as a web server.
129
157
  It strives to have zero magic and to be as explicit as possible.
130
158
  email:
131
159
  - lud@reinmiedl.com
@@ -140,7 +168,6 @@ files:
140
168
  - README.md
141
169
  - bin/kirei
142
170
  - kirei.gemspec
143
- - lib/boot.rb
144
171
  - lib/cli.rb
145
172
  - lib/cli/commands/new_app/base_directories.rb
146
173
  - lib/cli/commands/new_app/execute.rb
@@ -154,16 +181,30 @@ files:
154
181
  - lib/cli/commands/start.rb
155
182
  - lib/kirei.rb
156
183
  - lib/kirei/app.rb
157
- - lib/kirei/app_base.rb
158
- - lib/kirei/base_controller.rb
159
- - lib/kirei/base_model.rb
160
184
  - lib/kirei/config.rb
185
+ - lib/kirei/controller.rb
186
+ - lib/kirei/errors/json_api_error.rb
187
+ - lib/kirei/errors/json_api_error_source.rb
161
188
  - lib/kirei/helpers.rb
162
- - lib/kirei/logger.rb
163
- - lib/kirei/middleware.rb
164
- - lib/kirei/router.rb
189
+ - lib/kirei/logging/level.rb
190
+ - lib/kirei/logging/logger.rb
191
+ - lib/kirei/logging/metric.rb
192
+ - lib/kirei/model.rb
193
+ - lib/kirei/model/base_class_interface.rb
194
+ - lib/kirei/model/class_methods.rb
195
+ - lib/kirei/model/human_id_generator.rb
196
+ - lib/kirei/routing/base.rb
197
+ - lib/kirei/routing/nilable_hooks_type.rb
198
+ - lib/kirei/routing/rack_env_type.rb
199
+ - lib/kirei/routing/rack_response_type.rb
200
+ - lib/kirei/routing/route.rb
201
+ - lib/kirei/routing/router.rb
202
+ - lib/kirei/routing/verb.rb
203
+ - lib/kirei/services/result.rb
204
+ - lib/kirei/services/runner.rb
165
205
  - lib/kirei/version.rb
166
206
  - sorbet/rbi/shims/base_model.rbi
207
+ - sorbet/rbi/shims/ruby.rbi
167
208
  homepage: https://github.com/swiknaba/kirei
168
209
  licenses:
169
210
  - MIT
@@ -185,9 +226,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
226
  - !ruby/object:Gem::Version
186
227
  version: '0'
187
228
  requirements: []
188
- rubygems_version: 3.5.6
229
+ rubygems_version: 3.5.9
189
230
  signing_key:
190
231
  specification_version: 4
191
- summary: Kirei is a strictly typed Ruby micro/REST-framework for building scaleable
192
- and performant microservices.
232
+ summary: Kirei is a typed Ruby micro/REST-framework for building scalable and performant
233
+ microservices.
193
234
  test_files: []
data/lib/boot.rb DELETED
@@ -1,23 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- # This is the entrypoint into the application,
5
- # This file loads first, hence we don't have Sorbet loaded yet.
6
-
7
- #
8
- # Load Order is important!
9
- #
10
-
11
- # First: check if all gems are installed correctly
12
- require "bundler/setup"
13
-
14
- # Second: load all gems (runtime dependencies only)
15
- require "logger"
16
- require "sorbet-runtime"
17
- require "oj"
18
- require "rack"
19
- require "pg"
20
- require "sequel" # "sequel_pg" is auto-required by "sequel"
21
-
22
- # Third: load all application code
23
- Dir[File.join(__dir__, "kirei/**/*.rb")].each { require(_1) }
@@ -1,72 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require_relative("app")
5
-
6
- module Kirei
7
- class AppBase < Kirei::App
8
- class << self
9
- extend T::Sig
10
-
11
- # convenience method since "Kirei.configuration" must be nilable since it is nil
12
- # at the beginning of initilization of the app
13
- sig { returns(Kirei::Config) }
14
- def config
15
- T.must(Kirei.configuration)
16
- end
17
-
18
- sig { returns(Pathname) }
19
- def root
20
- defined?(::APP_ROOT) ? Pathname.new(::APP_ROOT) : Pathname.new(Dir.pwd)
21
- end
22
-
23
- sig { returns(String) }
24
- def version
25
- @version = T.let(@version, T.nilable(String))
26
- @version ||= ENV.fetch("APP_VERSION", nil)
27
- @version ||= ENV.fetch("GIT_SHA", nil)
28
- @version ||= T.must(
29
- `command -v git && git rev-parse --short HEAD`.to_s.split("\n").last,
30
- ).freeze # localhost
31
- end
32
-
33
- sig { returns(String) }
34
- def environment
35
- ENV.fetch("RACK_ENV", "development")
36
- end
37
-
38
- sig { returns(String) }
39
- def default_db_name
40
- @default_db_name ||= T.let("#{config.app_name}_#{environment}".freeze, T.nilable(String))
41
- end
42
-
43
- sig { returns(String) }
44
- def default_db_url
45
- @default_db_url ||= T.let(
46
- ENV.fetch("DATABASE_URL", "postgresql://localhost:5432/#{default_db_name}"),
47
- T.nilable(String),
48
- )
49
- end
50
-
51
- sig { returns(Sequel::Database) }
52
- def raw_db_connection
53
- @raw_db_connection = T.let(@raw_db_connection, T.nilable(Sequel::Database))
54
- return @raw_db_connection unless @raw_db_connection.nil?
55
-
56
- # calling "Sequel.connect" creates a new connection
57
- @raw_db_connection = Sequel.connect(AppBase.config.db_url || default_db_url)
58
-
59
- config.db_extensions.each do |ext|
60
- T.cast(@raw_db_connection, Sequel::Database).extension(ext)
61
- end
62
-
63
- if config.db_extensions.include?(:pg_json)
64
- # https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb#L8
65
- @raw_db_connection.wrap_json_primitives = true
66
- end
67
-
68
- @raw_db_connection
69
- end
70
- end
71
- end
72
- end
@@ -1,16 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require_relative("app")
5
-
6
- module Kirei
7
- class BaseController < Kirei::App
8
- extend T::Sig
9
- # register(Sinatra::Namespace)
10
-
11
- # before do
12
- # Thread.current[:request_id] = request.env["HTTP_X_REQUEST_ID"].presence ||
13
- # "req_#{AppBase.environment}_#{SecureRandom.uuid}"
14
- # end
15
- end
16
- end
data/lib/kirei/logger.rb DELETED
@@ -1,196 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module Kirei
5
- # rubocop:disable Metrics
6
-
7
- #
8
- # Example Usage:
9
- #
10
- # Kirei::Logger.call(
11
- # level: :info,
12
- # label: "Request started",
13
- # meta: {
14
- # key: "value",
15
- # },
16
- # )
17
- #
18
- # You can define a custom log transformer to transform the logline:
19
- #
20
- # Kirei::AppBase.config.log_transformer = Proc.new { _1 }
21
- #
22
- # By default, "meta" is flattened, and sensitive values are masked using see `Kirei::AppBase.config.sensitive_keys`.
23
- # You can also build on top of the provided log transformer:
24
- #
25
- # Kirei::AppBase.config.log_transformer = Proc.new do |meta|
26
- # flattened_meta = Kirei::Logger.flatten_hash_and_mask_sensitive_values(meta)
27
- # # Do something with the flattened meta
28
- # flattened_meta.map { _1.to_json }
29
- # end
30
- #
31
- # NOTE: The log transformer must return an array of strings to allow emitting multiple lines per log event.
32
- #
33
- class Logger
34
- extend T::Sig
35
-
36
- FILTERED = "[FILTERED]"
37
-
38
- @instance = T.let(nil, T.nilable(Kirei::Logger))
39
-
40
- sig { void }
41
- def initialize
42
- super
43
- @queue = T.let(Thread::Queue.new, Thread::Queue)
44
- @thread = T.let(start_logging_thread, Thread)
45
- end
46
-
47
- sig { returns(Kirei::Logger) }
48
- def self.instance
49
- @instance ||= new
50
- end
51
-
52
- sig { returns(::Logger) }
53
- def self.logger
54
- return @logger unless @logger.nil?
55
-
56
- @logger = T.let(nil, T.nilable(::Logger))
57
- @logger ||= ::Logger.new($stdout)
58
-
59
- # we want the logline to be parseable to JSON
60
- @logger.formatter = proc do |_severity, _datetime, _progname, msg|
61
- "#{msg}\n"
62
- end
63
-
64
- @logger
65
- end
66
-
67
- sig do
68
- params(
69
- level: T.any(String, Symbol),
70
- label: String,
71
- meta: T::Hash[Symbol, T.untyped],
72
- ).void
73
- end
74
- def self.call(level:, label:, meta: {})
75
- return if ENV["LOGGER"] == "disabled"
76
-
77
- instance.call(level: level, label: label, meta: meta)
78
- end
79
-
80
- sig do
81
- params(
82
- level: T.any(String, Symbol),
83
- label: String,
84
- meta: T::Hash[Symbol, T.untyped],
85
- ).void
86
- end
87
- def call(level:, label:, meta: {})
88
- Kirei::AppBase.config.log_default_metadata.each_pair do |key, value|
89
- meta[key] ||= value
90
- end
91
-
92
- #
93
- # key names follow OpenTelemetry Semantic Conventions
94
- # Source: https://opentelemetry.io/docs/concepts/semantic-conventions/
95
- #
96
- meta[:"service.instance.id"] ||= Thread.current[:request_id]
97
- meta[:"service.name"] ||= Kirei::AppBase.config.app_name
98
-
99
- # The Ruby logger only accepts one string as the only argument
100
- @queue << { level: level, label: label, meta: meta }
101
- end
102
-
103
- sig { returns(Thread) }
104
- def start_logging_thread
105
- Thread.new do
106
- Kernel.loop do
107
- log_data = T.let(@queue.pop, T::Hash[Symbol, T.untyped])
108
- level = log_data.fetch(:level)
109
- label = log_data.fetch(:label)
110
- meta = T.let(log_data.fetch(:meta), T::Hash[Symbol, T.untyped])
111
- meta[:"service.version"] ||= Kirei::AppBase.version
112
- meta[:timestamp] ||= Time.now.utc.iso8601
113
- meta[:level] ||= level.to_s.upcase
114
- meta[:label] ||= label
115
-
116
- log_transformer = AppBase.config.log_transformer
117
-
118
- loglines = if log_transformer
119
- log_transformer.call(meta)
120
- else
121
- [Oj.dump(
122
- Kirei::Logger.flatten_hash_and_mask_sensitive_values(meta),
123
- Kirei::OJ_OPTIONS,
124
- )]
125
- end
126
-
127
- loglines.each { Kirei::Logger.logger.error(_1) }
128
- end
129
- end
130
- end
131
-
132
- # rubocop:disable Naming/MethodParameterName
133
- sig do
134
- params(
135
- k: Symbol,
136
- v: String,
137
- ).returns(String)
138
- end
139
- def self.mask(k, v)
140
- return Kirei::Logger::FILTERED if AppBase.config.sensitive_keys.any? { k.match?(_1) }
141
-
142
- v
143
- end
144
- # rubocop:enable Naming/MethodParameterName
145
-
146
- sig do
147
- params(
148
- hash: T::Hash[T.any(Symbol, String), T.untyped],
149
- prefix: Symbol,
150
- ).returns(T::Hash[Symbol, T.untyped])
151
- end
152
- def self.flatten_hash_and_mask_sensitive_values(hash, prefix = :'')
153
- result = T.let({}, T::Hash[Symbol, T.untyped])
154
- Kirei::Helpers.deep_symbolize_keys!(hash)
155
- hash = T.cast(hash, T::Hash[Symbol, T.untyped])
156
-
157
- hash.each do |key, value|
158
- new_prefix = Kirei::Helpers.blank?(prefix) ? key : :"#{prefix}.#{key}"
159
-
160
- case value
161
- when Hash
162
- # Some libraries have a custom Hash class that inhert from Hash, but act differently, e.g. OmniAuth::AuthHash.
163
- # This results in `transform_keys` being available but without any effect.
164
- value = value.to_h if value.class != Hash
165
- result.merge!(flatten_hash_and_mask_sensitive_values(value.transform_keys(&:to_sym), new_prefix))
166
- when Array
167
- value.each_with_index do |element, index|
168
- if element.is_a?(Hash) || element.is_a?(Array)
169
- result.merge!(flatten_hash_and_mask_sensitive_values({ index => element }, new_prefix))
170
- else
171
- result[:"#{new_prefix}.#{index}"] = element.is_a?(String) ? mask(key, element) : element
172
- end
173
- end
174
- when String then result[new_prefix] = mask(key, value)
175
- when Numeric, FalseClass, TrueClass, NilClass then result[new_prefix] = value
176
- else
177
- if value.respond_to?(:serialize)
178
- serialized_value = value.serialize
179
- if serialized_value.is_a?(Hash)
180
- result.merge!(
181
- flatten_hash_and_mask_sensitive_values(serialized_value.transform_keys(&:to_sym), new_prefix),
182
- )
183
- else
184
- result[new_prefix] = serialized_value&.to_s
185
- end
186
- else
187
- result[new_prefix] = value&.to_s
188
- end
189
- end
190
- end
191
-
192
- result
193
- end
194
- end
195
- # rubocop:enable Metrics
196
- end
data/lib/kirei/router.rb DELETED
@@ -1,61 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require("singleton")
5
-
6
- module Kirei
7
- #
8
- # Usage:
9
- #
10
- # Router.add_routes([
11
- # Route.new(
12
- # verb: "GET",
13
- # path: "/livez",
14
- # controller: Controllers::HealthController,
15
- # action: "livez",
16
- # ),
17
- # ])
18
- #
19
- class Router
20
- extend T::Sig
21
- include ::Singleton
22
-
23
- class Route < T::Struct
24
- const :verb, String
25
- const :path, String
26
- const :controller, T.class_of(BaseController)
27
- const :action, String
28
- end
29
-
30
- RoutesHash = T.type_alias do
31
- T::Hash[String, Route]
32
- end
33
-
34
- sig { void }
35
- def initialize
36
- @routes = T.let({}, RoutesHash)
37
- end
38
-
39
- sig { returns(RoutesHash) }
40
- attr_reader :routes
41
-
42
- sig do
43
- params(
44
- verb: String,
45
- path: String,
46
- ).returns(T.nilable(Route))
47
- end
48
- def get(verb, path)
49
- key = "#{verb} #{path}"
50
- routes[key]
51
- end
52
-
53
- sig { params(routes: T::Array[Route]).void }
54
- def self.add_routes(routes)
55
- routes.each do |route|
56
- key = "#{route.verb} #{route.path}"
57
- instance.routes[key] = route
58
- end
59
- end
60
- end
61
- end