kirei 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8beff5327a463020ae69872995aa9a5101e492f23fa886f6a29e45cfb173728c
4
+ data.tar.gz: a2ec68b1e7e7f182d42587d4e73b3736636d883b7d5f93a804956366368467d2
5
+ SHA512:
6
+ metadata.gz: 7bc7eef26e4ed940d91a425c69a447a585a76c2512132b1986b927652bc5331dddd6e77fc279ee65d7739e9d991bd1881c8c421023e1b4b25f70d5ae0224bd79
7
+ data.tar.gz: 4f1db54baab633700669e6f7d8f153e73a1f5383ebd9b4c593acd95956699c8bc6b8bafa2d4194aa9a00e13a74bea9bf106a631d48c147d9b60f3f912d75f58d
data/.irbrc ADDED
@@ -0,0 +1 @@
1
+ require_relative "./lib/kirei" # rubocop:disable all
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-09-02
4
+
5
+ - Initial release
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Kirei
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.
4
+
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
+
7
+ > Kirei (きれい) is a Japanese adjective that primarily means "beautiful" or "pretty." It can also be used to describe something that is "clean" or "neat."
8
+
9
+ ## Why another Ruby framework?
10
+
11
+ TL;DR:
12
+
13
+ * **zero magic**
14
+ * **strict typing**
15
+ * **very few low level dependencies**
16
+ * ultra low memory footprint
17
+ * high performance
18
+ * simple to understand
19
+
20
+ ## Versioning
21
+
22
+ This gem follows SemVer, however only after a stable release 1.0.0 is made.
23
+
24
+ A changelog is maintained in the [CHANGELOG.md](CHANGELOG.md) file and/or the GitHub Releases page.
25
+
26
+ ## Installation
27
+
28
+ Via rubygems:
29
+
30
+ ```ruby
31
+ gem 'kirei'
32
+ ```
33
+
34
+ Test the latest version via git:
35
+
36
+ ```ruby
37
+ gem 'kirei', git: 'git@github.com:swiknaba/kirei', branch: :main
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Initial Set Up
43
+
44
+ Scaffold a new project:
45
+
46
+ ```shell
47
+ bundle exec kirei new "MyApp"
48
+ ```
49
+
50
+ ### Quick Start
51
+
52
+ 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)`.
53
+
54
+ ```ruby
55
+ class User < T::Struct
56
+ extend T::Sig
57
+ include Kirei::BaseModel
58
+
59
+ const :id, T.any(String, Integer)
60
+ const :name, String
61
+ end
62
+
63
+ user = User.find_by({ name: 'John' }) # T.nilable(User)
64
+ users = User.where({ name: 'John' }) # T::Array[User]
65
+ ```
66
+
67
+ Updating a record returns a new instance. The original instance is not mutated:
68
+
69
+ ```ruby
70
+ updated_user = user.update({ name: 'Johnny' })
71
+ user.name # => 'John'
72
+ updated_user.name # => 'Johnny'
73
+ ```
74
+
75
+ To build more complex queries, Sequel can be used directly:
76
+
77
+ ```ruby
78
+ query = User.db.where({ name: 'John' })
79
+ query = query.where('...')
80
+ query = query.limit(10) # query is a Sequel::Dataset, chain as you like
81
+
82
+ users = User.resolve(query) # T::Array[User]
83
+ first_user = User.resolve_first(query) # T.nilable(User)
84
+
85
+ # you can also cast the raw result manually
86
+ first_user = User.from_hash(query.first.stringify_keys)
87
+ ```
88
+
89
+ ## Contributions
90
+
91
+ 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.
92
+
93
+ Please commit small and focused PRs with descriptive commit messages. If you are unsure about a PR, please open a draft PR to get early feedback. A PR must have a short description ("what"), a motiviation ("why"), and, if applicable, instructions how to test the changes, measure performance improvements, etc.
94
+
95
+ ## Publishing a new version
96
+
97
+ run
98
+
99
+ ```shell
100
+ bin/release
101
+ ```
102
+
103
+ which will guide you through the release process.
data/bin/kirei ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/cli'
3
+
4
+ Cli::Commands::Start.call(ARGV)
data/kirei.gemspec ADDED
@@ -0,0 +1,62 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "kirei/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "kirei"
7
+ spec.version = Kirei::VERSION
8
+ spec.authors = [
9
+ "Ludwig Reinmiedl",
10
+ ]
11
+ spec.email = [
12
+ "lud@reinmiedl.com",
13
+ "oss@dbl.works",
14
+ ]
15
+
16
+ spec.summary = "Kirei is a strictly typed Ruby micro/REST-framework for building scaleable and performant APIs."
17
+ spec.description = <<~TXT
18
+ Kirei's structure und developer experience is inspired by Rails, but it's not a Rails clone.
19
+ It's a framework that's built from the ground up to be strictly typed, performant and scaleable.
20
+ TXT
21
+ spec.homepage = "https://github.com/swiknaba/kirei"
22
+ spec.license = "MIT"
23
+ spec.required_ruby_version = ">= 3.0.0"
24
+
25
+ spec.metadata["rubygems_mfa_required"] = "true"
26
+ spec.metadata["homepage_uri"] = spec.homepage
27
+
28
+ spec.files = Dir[
29
+ "kirei.gemspec",
30
+ ".irbrc",
31
+ "lib/**/*",
32
+ "sorbet/rbi/dsl/**/*.rbi",
33
+ "sorbet/rbi/shims/**/*.rbi",
34
+ "LICENSE",
35
+ "README.md",
36
+ "CHANGELOG.md",
37
+ ]
38
+
39
+ spec.bindir = "bin"
40
+ spec.executables = [File.basename("bin/kirei")]
41
+ spec.require_paths = ["lib"]
42
+
43
+ # Utilities
44
+ spec.add_dependency "activesupport", "~> 6.0"
45
+ spec.add_dependency "oj", "~> 3.0"
46
+ spec.add_dependency "rake", "~> 13.0"
47
+ spec.add_dependency "sorbet-runtime", "~> 0.5"
48
+ spec.add_dependency "tzinfo-data", "~> 1.0" # for containerized environments, e.g. on AWS ECS
49
+
50
+ # Web server & routing
51
+ spec.add_dependency "puma", "~> 6.0"
52
+ spec.add_dependency "sinatra", "~> 3.0"
53
+ spec.add_dependency "sinatra-contrib", "~> 3.0"
54
+
55
+ # Database (Postgres)
56
+ spec.add_dependency "pg", "~> 1.0"
57
+ spec.add_dependency "sequel", "~> 5.0"
58
+ spec.add_dependency "sequel_pg", "~> 1.0"
59
+
60
+ # For more information and examples about making a new gem, check out our
61
+ # guide at: https://bundler.io/guides/creating_gem.html
62
+ end
data/lib/boot.rb ADDED
@@ -0,0 +1,31 @@
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 "sorbet-runtime"
16
+ require "oj"
17
+ require "active_support/all"
18
+ require "puma"
19
+ require "sinatra"
20
+ require "sinatra/namespace" # from sinatra-contrib
21
+ require "pg"
22
+ require "sequel"
23
+ # "sequel_pg" should be auto-required by "sequel"
24
+
25
+ Oj.default_options = {
26
+ mode: :compat, # required to dump hashes with symbol-keys
27
+ symbol_keys: false, # T::Struct.new works only with string-keys
28
+ }
29
+
30
+ # Third: load all application code
31
+ Dir[File.join(__dir__, "kirei/**/*.rb")].each { require(_1) }
@@ -0,0 +1,42 @@
1
+ # typed: false
2
+
3
+ module Cli
4
+ module Commands
5
+ module NewApp
6
+ class BaseDirectories
7
+ def self.call
8
+ directories.each do |dir|
9
+ FileUtils.mkdir_p(dir)
10
+ end
11
+ end
12
+
13
+ def self.directories
14
+ [
15
+ "app",
16
+ "app/controllers",
17
+ "app/models",
18
+ "app/services",
19
+
20
+ "bin",
21
+ "config",
22
+ "config/initializers",
23
+
24
+ "db",
25
+ "db/migrate",
26
+ "db/seeds",
27
+
28
+ "sorbet",
29
+ "sorbet/rbi",
30
+ "sorbet/rbi/shims",
31
+ "sorbet/tapioca",
32
+
33
+ "spec",
34
+ "spec/factories",
35
+ "spec/fixtures",
36
+ "spec/support",
37
+ ]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,20 @@
1
+ # typed: false
2
+
3
+ require "fileutils"
4
+ require "active_support/all"
5
+
6
+ module Cli
7
+ module Commands
8
+ module NewApp
9
+ class Execute
10
+ def self.call(app_name:)
11
+ BaseDirectories.call
12
+ Files::App.call(app_name)
13
+ Files::Irbrc.call
14
+
15
+ puts "Kirei app '#{app_name}' scaffolded successfully!" # rubocop:disable all
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # typed: false
2
+
3
+ module Cli
4
+ module Commands
5
+ module NewApp
6
+ module Files
7
+ class App
8
+ def self.call(app_name)
9
+ File.write("app.rb", content(app_name))
10
+ end
11
+
12
+ def self.content(app_name)
13
+ <<~RUBY
14
+ # typed: true
15
+ # frozen_string_literal: true
16
+
17
+ class #{app_name} < Kirei::Base
18
+ end
19
+ RUBY
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # typed: false
2
+
3
+ module Cli
4
+ module Commands
5
+ module NewApp
6
+ module Files
7
+ class Irbrc
8
+ def self.call
9
+ File.write(".irbrc", content)
10
+ end
11
+
12
+ def self.content
13
+ <<~RUBY
14
+ # frozen_string_literal: true
15
+
16
+ # Kirei needs to know where the root of the project is
17
+ APP_ROOT = File.expand_path(__dir__)
18
+
19
+ ENV['RACK_ENV'] ||= 'development'
20
+ ENV['APP_VERSION'] ||= (ENV['GIT_SHA'] ||= `git rev-parse --short HEAD`.to_s.chomp.freeze)
21
+ require('dotenv/load') if %w[test development].include?(ENV['RACK_ENV'])
22
+ require_relative('app')
23
+ RUBY
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # typed: false
2
+
3
+ require "fileutils"
4
+ require "active_support/all"
5
+
6
+ module Cli
7
+ module Commands
8
+ class Start
9
+ def self.call(args)
10
+ case args[0]
11
+ when "new"
12
+ app_name = args[1] || "MyApp"
13
+ app_name = app_name.gsub(/[-\s]/, "_").classify
14
+ NewApp::Execute.call(app_name: app_name)
15
+ else
16
+ puts "Unknown command" # rubocop:disable all
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/cli.rb ADDED
@@ -0,0 +1,9 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "kirei"
5
+
6
+ Dir[File.join(__dir__, "cli/**/*.rb")].each { require(_1) }
7
+
8
+ module Cli
9
+ end
@@ -0,0 +1,65 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ class AppBase < ::Sinatra::Base
6
+ class << self
7
+ extend T::Sig
8
+
9
+ # convenience method since "Kirei.configuration" must be nilable since it is nil
10
+ # at the beginning of initilization of the app
11
+ sig { returns(Kirei::Config) }
12
+ def config
13
+ T.must(Kirei.configuration)
14
+ end
15
+
16
+ sig { returns(Pathname) }
17
+ def root
18
+ defined?(::APP_ROOT) ? Pathname.new(::APP_ROOT) : Pathname.new(Dir.pwd)
19
+ end
20
+
21
+ sig { returns(String) }
22
+ def version
23
+ @version = T.let(@version, T.nilable(String))
24
+ @version ||= ENV.fetch("APP_VERSION", nil)
25
+ @version ||= ENV.fetch("GIT_SHA", nil)
26
+ @version ||= T.must(
27
+ `command -v git && git rev-parse --short HEAD`.to_s.split("\n").last,
28
+ ).freeze # localhost
29
+ end
30
+
31
+ sig { returns(String) }
32
+ def env
33
+ ENV.fetch("RACK_ENV", "development")
34
+ end
35
+
36
+ sig { returns(String) }
37
+ def default_db_name
38
+ @default_db_name ||= T.let("#{config.app_name}_#{env}".freeze, T.nilable(String))
39
+ end
40
+
41
+ sig { returns(String) }
42
+ def default_db_url
43
+ @default_db_url ||= T.let(
44
+ ENV.fetch("DATABASE_URL", "postgresql://localhost:5432/#{default_db_name}"),
45
+ T.nilable(String),
46
+ )
47
+ end
48
+
49
+ sig { returns(Sequel::Database) }
50
+ def raw_db_connection
51
+ @raw_db_connection = T.let(@raw_db_connection, T.nilable(Sequel::Database))
52
+ return @raw_db_connection unless @raw_db_connection.nil?
53
+
54
+ # calling "Sequel.connect" creates a new connection
55
+ @raw_db_connection = Sequel.connect(AppBase.config.db_url || default_db_url)
56
+
57
+ config.db_extensions.each do |ext|
58
+ @raw_db_connection.extension(ext)
59
+ end
60
+
61
+ @raw_db_connection
62
+ end
63
+ end
64
+ end
65
+ end
data/lib/kirei/base.rb ADDED
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ class Base
6
+ extend T::Sig
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ class BaseController < Sinatra::Base
6
+ extend T::Sig
7
+ register(Sinatra::Namespace)
8
+
9
+ before do
10
+ Thread.current[:request_id] = request.env["HTTP_X_REQUEST_ID"].presence ||
11
+ "req_#{AppBase.env}_#{SecureRandom.uuid}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,127 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ module BaseModel
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ sig { returns(BaseClassInterface) }
10
+ def class; super; end # rubocop:disable all
11
+
12
+ # An update keeps the original object intact, and returns a new object with the updated values.
13
+ sig do
14
+ params(
15
+ hash: T::Hash[Symbol, T.untyped],
16
+ ).returns(T.self_type)
17
+ end
18
+ def update(hash)
19
+ self.class.db.where({ id: id }).update(hash)
20
+ self.class.find_by({ id: id })
21
+ end
22
+
23
+ module BaseClassInterface
24
+ extend T::Sig
25
+ extend T::Helpers
26
+ interface!
27
+
28
+ sig { abstract.params(hash: T.untyped).returns(T.untyped) }
29
+ def find_by(hash)
30
+ end
31
+
32
+ sig { abstract.params(hash: T.untyped).returns(T.untyped) }
33
+ def where(hash)
34
+ end
35
+
36
+ sig { abstract.params(hash: T.untyped).returns(T.untyped) }
37
+ def resolve(hash)
38
+ end
39
+
40
+ sig { abstract.params(hash: T.untyped).returns(T.untyped) }
41
+ def resolve_first(hash)
42
+ end
43
+
44
+ sig { abstract.returns(T.untyped) }
45
+ def table_name
46
+ end
47
+
48
+ sig { abstract.returns(T.untyped) }
49
+ def db
50
+ end
51
+ end
52
+
53
+ module ClassMethods
54
+ extend T::Sig
55
+ extend T::Generic
56
+
57
+ # the attached class is the class that extends this module
58
+ # e.g. "User"
59
+ # extend T::Generic
60
+ # has_attached_class!
61
+ has_attached_class!
62
+
63
+ include BaseClassInterface
64
+
65
+ sig { override.returns(String) }
66
+ def table_name
67
+ T.must(name.split("::").last).pluralize.underscore
68
+ end
69
+
70
+ sig { override.returns(Sequel::Dataset) }
71
+ def db
72
+ AppBase.raw_db_connection[table_name.to_sym]
73
+ end
74
+
75
+ sig do
76
+ override.params(
77
+ hash: T::Hash[Symbol, T.untyped],
78
+ ).returns(T::Array[T.attached_class])
79
+ end
80
+ def where(hash)
81
+ resolve(db.where(hash))
82
+ end
83
+
84
+ sig do
85
+ override.params(
86
+ hash: T::Hash[Symbol, T.untyped],
87
+ ).returns(T.nilable(T.attached_class))
88
+ end
89
+ def find_by(hash)
90
+ resolve_first(db.where(hash))
91
+ end
92
+
93
+ # Extra or unknown properties present in the Hash do not raise exceptions at
94
+ # runtime unless the optional strict argument to from_hash is passed
95
+ #
96
+ # Source: https://sorbet.org/docs/tstruct#from_hash-gotchas
97
+ # "strict" defaults to "false".
98
+ sig do
99
+ override.params(
100
+ query: Sequel::Dataset,
101
+ strict: T.nilable(T::Boolean),
102
+ ).returns(T::Array[T.attached_class])
103
+ end
104
+ def resolve(query, strict = nil)
105
+ strict_loading = strict.nil? ? AppBase.config.db_strict_type_resolving : strict
106
+
107
+ query.map do |row|
108
+ row = T.cast(row, T::Hash[Symbol, T.untyped])
109
+ row.stringify_keys! # sequel returns symbolized keys
110
+ from_hash(row, strict_loading)
111
+ end
112
+ end
113
+
114
+ sig do
115
+ override.params(
116
+ query: Sequel::Dataset,
117
+ strict: T.nilable(T::Boolean),
118
+ ).returns(T.nilable(T.attached_class))
119
+ end
120
+ def resolve_first(query, strict = nil)
121
+ resolve(query.limit(1), strict).first
122
+ end
123
+ end
124
+
125
+ mixes_in_class_methods(ClassMethods)
126
+ end
127
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ class Config < T::Struct
6
+ extend T::Sig
7
+
8
+ SENSITIVE_KEYS = T.let(
9
+ [
10
+ # address data
11
+ /email|first_name|last_name|full_name|city|country_alpha2|country_name|country|zip_code/,
12
+ # auth data
13
+ /password|password_confirmation|access_token|client_secret|client_secret_ciphertext|client_key|token/,
14
+ ].freeze,
15
+ T::Array[Regexp],
16
+ )
17
+
18
+ prop :logger, ::Logger, factory: -> { ::Logger.new($stdout) }
19
+ prop :log_transformer, T.nilable(T.proc.params(msg: T::Hash[Symbol, T.untyped]).returns(T::Array[String]))
20
+ # dup to allow the user to extend the existing list of sensitive keys
21
+ prop :sensitive_keys, T::Array[Regexp], factory: -> { SENSITIVE_KEYS.dup }
22
+ prop :app_name, String, default: "kirei"
23
+ prop :db_url, T.nilable(String)
24
+
25
+ # must use "pg_json" to parse jsonb columns to hashes
26
+ #
27
+ # Source: https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb
28
+ prop :db_extensions, T::Array[Symbol], default: %i[pg_json pg_array]
29
+
30
+ # Extra or unknown properties present in the Hash do not raise exceptions at runtime
31
+ # unless the optional strict argument to from_hash is passed
32
+ #
33
+ # Source: https://sorbet.org/docs/tstruct#from_hash-gotchas
34
+ prop :db_strict_type_resolving, T.nilable(T::Boolean), default: nil
35
+ end
36
+ end
@@ -0,0 +1,175 @@
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.config.log_transformer = Proc.new { _1 }
21
+ #
22
+ # By default, "meta" is flattened, and sensitive values are masked using see `Kirei.config.sensitive_keys`.
23
+ # You can also build on top of the provided log transformer:
24
+ #
25
+ # Kirei.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 < Kirei::Base
34
+ FILTERED = "[FILTERED]"
35
+
36
+ @instance = T.let(nil, T.nilable(Kirei::Logger))
37
+
38
+ sig { void }
39
+ def initialize
40
+ super
41
+ @queue = T.let(Thread::Queue.new, Thread::Queue)
42
+ @thread = T.let(start_logging_thread, Thread)
43
+ end
44
+
45
+ sig { returns(Kirei::Logger) }
46
+ def self.instance
47
+ @instance ||= new
48
+ end
49
+
50
+ sig { returns(::Logger) }
51
+ def self.logger
52
+ return @logger unless @logger.nil?
53
+
54
+ @logger = T.let(nil, T.nilable(::Logger))
55
+ @logger ||= ::Logger.new($stdout)
56
+
57
+ # we want the logline to be parseable to JSON
58
+ @logger.formatter = proc do |_severity, _datetime, _progname, msg|
59
+ "#{msg}\n"
60
+ end
61
+
62
+ @logger
63
+ end
64
+
65
+ sig do
66
+ params(
67
+ level: T.any(String, Symbol),
68
+ label: String,
69
+ meta: T::Hash[Symbol, T.untyped],
70
+ ).void
71
+ end
72
+ def self.call(level:, label:, meta: {})
73
+ return if ENV["LOGGER"] == "disabled"
74
+
75
+ instance.call(level: level, label: label, meta: meta)
76
+ end
77
+
78
+ sig do
79
+ params(
80
+ level: T.any(String, Symbol),
81
+ label: String,
82
+ meta: T::Hash[Symbol, T.untyped],
83
+ ).void
84
+ end
85
+ def call(level:, label:, meta: {})
86
+ meta[:"service.instance.id"] ||= Thread.current[:request_id]
87
+
88
+ # The Ruby logger only accepts one string as the only argument
89
+ @queue << { level: level, label: label, meta: meta }
90
+ end
91
+
92
+ sig { returns(Thread) }
93
+ def start_logging_thread
94
+ Thread.new do
95
+ Kernel.loop do
96
+ log_data = T.let(@queue.pop, T::Hash[Symbol, T.untyped])
97
+ level = log_data.fetch(:level)
98
+ label = log_data.fetch(:label)
99
+ meta = T.let(log_data.fetch(:meta), T::Hash[Symbol, T.untyped])
100
+ meta[:"service.version"] ||= Kirei::AppBase.version
101
+ meta[:timestamp] ||= Time.current.utc.iso8601
102
+ meta[:level] ||= level.to_s.upcase
103
+ meta[:label] ||= label
104
+
105
+ log_transformer = AppBase.config.log_transformer
106
+
107
+ loglines = if log_transformer
108
+ log_transformer.call(meta)
109
+ else
110
+ [Oj.dump(Kirei::Logger.flatten_hash_and_mask_sensitive_values(meta))]
111
+ end
112
+
113
+ loglines.each { Kirei::Logger.logger.error(_1) }
114
+ end
115
+ end
116
+ end
117
+
118
+ # rubocop:disable Naming/MethodParameterName
119
+ sig do
120
+ params(
121
+ k: Symbol,
122
+ v: String,
123
+ ).returns(String)
124
+ end
125
+ def self.mask(k, v)
126
+ return Kirei::Logger::FILTERED if AppBase.config.sensitive_keys.any? { k.match?(_1) }
127
+
128
+ v
129
+ end
130
+ # rubocop:enable Naming/MethodParameterName
131
+
132
+ sig do
133
+ params(
134
+ hash: T::Hash[Symbol, T.untyped],
135
+ prefix: Symbol,
136
+ ).returns(T::Hash[Symbol, T.untyped])
137
+ end
138
+ def self.flatten_hash_and_mask_sensitive_values(hash, prefix = :'')
139
+ result = T.let({}, T::Hash[Symbol, T.untyped])
140
+ hash.deep_symbolize_keys!
141
+
142
+ hash.each do |key, value|
143
+ new_prefix = prefix.blank? ? key : :"#{prefix}.#{key}"
144
+
145
+ case value
146
+ when Hash then result.merge!(flatten_hash_and_mask_sensitive_values(value.symbolize_keys, new_prefix))
147
+ when Array
148
+ value.each_with_index do |element, index|
149
+ if element.is_a?(Hash) || element.is_a?(Array)
150
+ result.merge!(flatten_hash_and_mask_sensitive_values({ index => element }, new_prefix))
151
+ else
152
+ result[:"#{new_prefix}.#{index}"] = element.is_a?(String) ? mask(key, element) : element
153
+ end
154
+ end
155
+ when String then result[new_prefix] = mask(key, value)
156
+ when Numeric, FalseClass, TrueClass, NilClass then result[new_prefix] = value
157
+ else
158
+ if value.respond_to?(:serialize)
159
+ serialized_value = value.serialize
160
+ if serialized_value.is_a?(Hash)
161
+ result.merge!(flatten_hash_and_mask_sensitive_values(serialized_value.symbolize_keys, new_prefix))
162
+ else
163
+ result[new_prefix] = serialized_value&.to_s
164
+ end
165
+ else
166
+ result[new_prefix] = value&.to_s
167
+ end
168
+ end
169
+ end
170
+
171
+ result
172
+ end
173
+ end
174
+ # rubocop:enable Metrics
175
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ VERSION = "0.0.1"
6
+ end
data/lib/kirei.rb ADDED
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ puts "Booting Kirei..." # rubocop:disable all
5
+
6
+ require "boot"
7
+
8
+ module Kirei
9
+ extend T::Sig
10
+
11
+ GEM_ROOT = T.let(
12
+ Gem::Specification.find_by_name("kirei").gem_dir,
13
+ String,
14
+ )
15
+
16
+ class << self
17
+ extend T::Sig
18
+
19
+ sig { returns(T.nilable(Kirei::Config)) }
20
+ attr_accessor :configuration
21
+
22
+ sig do
23
+ params(
24
+ _: T.proc.params(configuration: Kirei::Config).void,
25
+ ).void
26
+ end
27
+ def configure(&_)
28
+ self.configuration ||= Kirei::Config.new
29
+ yield(T.must(configuration))
30
+ end
31
+ end
32
+ end
33
+
34
+ Kirei.configure(&:itself)
35
+
36
+ puts "Kirei (#{Kirei::VERSION}) booted!" # rubocop:disable all
@@ -0,0 +1,22 @@
1
+ # typed: true
2
+
3
+ # DO NOT EDIT MANUALLY
4
+ # This is an autogenerated file for dynamic methods in `ActiveSupport::Callbacks`.
5
+ # Please instead update this file by running `bin/tapioca dsl ActiveSupport::Callbacks`.
6
+
7
+ module ActiveSupport::Callbacks
8
+ include GeneratedInstanceMethods
9
+
10
+ mixes_in_class_methods GeneratedClassMethods
11
+
12
+ module GeneratedClassMethods
13
+ def __callbacks; end
14
+ def __callbacks=(value); end
15
+ def __callbacks?; end
16
+ end
17
+
18
+ module GeneratedInstanceMethods
19
+ def __callbacks; end
20
+ def __callbacks?; end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # typed: true
2
+
3
+ # rubocop:disable Style/EmptyMethod
4
+ module Kirei
5
+ module BaseModel
6
+ sig { returns(T.any(String, Integer)) }
7
+ def id; end
8
+
9
+ module ClassMethods
10
+ include T::Props::Serializable::ClassMethods
11
+
12
+ sig { returns(String) }
13
+ def name; end
14
+ end
15
+ end
16
+ end
17
+ # rubocop:enable Style/EmptyMethod
metadata ADDED
@@ -0,0 +1,226 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kirei
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ludwig Reinmiedl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-12-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: oj
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sorbet-runtime
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.5'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tzinfo-data
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: puma
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '6.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '6.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sinatra
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sinatra-contrib
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pg
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sequel
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '5.0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '5.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: sequel_pg
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '1.0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.0'
167
+ description: |
168
+ Kirei's structure und developer experience is inspired by Rails, but it's not a Rails clone.
169
+ It's a framework that's built from the ground up to be strictly typed, performant and scaleable.
170
+ email:
171
+ - lud@reinmiedl.com
172
+ - oss@dbl.works
173
+ executables:
174
+ - kirei
175
+ extensions: []
176
+ extra_rdoc_files: []
177
+ files:
178
+ - ".irbrc"
179
+ - CHANGELOG.md
180
+ - README.md
181
+ - bin/kirei
182
+ - kirei.gemspec
183
+ - lib/boot.rb
184
+ - lib/cli.rb
185
+ - lib/cli/commands/new_app/base_directories.rb
186
+ - lib/cli/commands/new_app/execute.rb
187
+ - lib/cli/commands/new_app/files/app.rb
188
+ - lib/cli/commands/new_app/files/irbrc.rb
189
+ - lib/cli/commands/start.rb
190
+ - lib/kirei.rb
191
+ - lib/kirei/app_base.rb
192
+ - lib/kirei/base.rb
193
+ - lib/kirei/base_controller.rb
194
+ - lib/kirei/base_model.rb
195
+ - lib/kirei/config.rb
196
+ - lib/kirei/logger.rb
197
+ - lib/kirei/version.rb
198
+ - sorbet/rbi/dsl/active_support/callbacks.rbi
199
+ - sorbet/rbi/shims/base_model.rbi
200
+ homepage: https://github.com/swiknaba/kirei
201
+ licenses:
202
+ - MIT
203
+ metadata:
204
+ rubygems_mfa_required: 'true'
205
+ homepage_uri: https://github.com/swiknaba/kirei
206
+ post_install_message:
207
+ rdoc_options: []
208
+ require_paths:
209
+ - lib
210
+ required_ruby_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: 3.0.0
215
+ required_rubygems_version: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - ">="
218
+ - !ruby/object:Gem::Version
219
+ version: '0'
220
+ requirements: []
221
+ rubygems_version: 3.5.3
222
+ signing_key:
223
+ specification_version: 4
224
+ summary: Kirei is a strictly typed Ruby micro/REST-framework for building scaleable
225
+ and performant APIs.
226
+ test_files: []