kirei 0.0.1

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