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 +7 -0
- data/.irbrc +1 -0
- data/CHANGELOG.md +5 -0
- data/README.md +103 -0
- data/bin/kirei +4 -0
- data/kirei.gemspec +62 -0
- data/lib/boot.rb +31 -0
- data/lib/cli/commands/new_app/base_directories.rb +42 -0
- data/lib/cli/commands/new_app/execute.rb +20 -0
- data/lib/cli/commands/new_app/files/app.rb +25 -0
- data/lib/cli/commands/new_app/files/irbrc.rb +29 -0
- data/lib/cli/commands/start.rb +21 -0
- data/lib/cli.rb +9 -0
- data/lib/kirei/app_base.rb +65 -0
- data/lib/kirei/base.rb +8 -0
- data/lib/kirei/base_controller.rb +14 -0
- data/lib/kirei/base_model.rb +127 -0
- data/lib/kirei/config.rb +36 -0
- data/lib/kirei/logger.rb +175 -0
- data/lib/kirei/version.rb +6 -0
- data/lib/kirei.rb +36 -0
- data/sorbet/rbi/dsl/active_support/callbacks.rbi +22 -0
- data/sorbet/rbi/shims/base_model.rbi +17 -0
- metadata +226 -0
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
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
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,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,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
|
data/lib/kirei/config.rb
ADDED
@@ -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
|
data/lib/kirei/logger.rb
ADDED
@@ -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
|
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: []
|