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
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