kirei 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Kirei
5
- module BaseModel
5
+ module Model
6
6
  extend T::Sig
7
7
  extend T::Helpers
8
8
 
@@ -115,7 +115,7 @@ module Kirei
115
115
 
116
116
  sig { override.returns(Sequel::Dataset) }
117
117
  def db
118
- AppBase.raw_db_connection[table_name.to_sym]
118
+ App.raw_db_connection[table_name.to_sym]
119
119
  end
120
120
 
121
121
  sig do
@@ -166,7 +166,7 @@ module Kirei
166
166
  def wrap_jsonb_non_primivitives!(attributes)
167
167
  # setting `@raw_db_connection.wrap_json_primitives = true`
168
168
  # only works on JSON primitives, but not on blank hashes/arrays
169
- return unless AppBase.config.db_extensions.include?(:pg_json)
169
+ return unless App.config.db_extensions.include?(:pg_json)
170
170
 
171
171
  attributes.each_pair do |key, value|
172
172
  next unless value.is_a?(Hash) || value.is_a?(Array)
@@ -196,7 +196,7 @@ module Kirei
196
196
  ).returns(T::Array[T.attached_class])
197
197
  end
198
198
  def resolve(query, strict = nil)
199
- strict_loading = strict.nil? ? AppBase.config.db_strict_type_resolving : strict
199
+ strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
200
200
 
201
201
  query.map do |row|
202
202
  row = T.cast(row, T::Hash[Symbol, T.untyped])
@@ -212,7 +212,7 @@ module Kirei
212
212
  ).returns(T.nilable(T.attached_class))
213
213
  end
214
214
  def resolve_first(query, strict = nil)
215
- strict_loading = strict.nil? ? AppBase.config.db_strict_type_resolving : strict
215
+ strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
216
216
 
217
217
  resolve(query.limit(1), strict_loading).first
218
218
  end
@@ -0,0 +1,156 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # rubocop:disable Metrics/all
5
+
6
+ module Kirei
7
+ module Routing
8
+ class Base
9
+ extend T::Sig
10
+
11
+ sig { params(params: T::Hash[String, T.untyped]).void }
12
+ def initialize(params: {})
13
+ @router = T.let(Router.instance, Router)
14
+ @params = T.let(params, T::Hash[String, T.untyped])
15
+ end
16
+
17
+ sig { returns(T::Hash[String, T.untyped]) }
18
+ attr_reader :params
19
+
20
+ sig { params(env: RackEnvType).returns(RackResponseType) }
21
+ def call(env)
22
+ http_verb = Router::Verb.deserialize(env.fetch("REQUEST_METHOD"))
23
+ req_path = T.cast(env.fetch("REQUEST_PATH"), String)
24
+ #
25
+ # TODO: reject requests from unexpected hosts -> allow configuring allowed hosts in a `cors.rb` file
26
+ # ( offer a scaffold for this file )
27
+ # -> use https://github.com/cyu/rack-cors ?
28
+ #
29
+
30
+ route = Router.instance.get(http_verb, req_path)
31
+ return [404, {}, ["Not Found"]] if route.nil?
32
+
33
+ params = case route.verb
34
+ when Router::Verb::GET
35
+ query = T.cast(env.fetch("QUERY_STRING"), String)
36
+ query.split("&").to_h do |p|
37
+ k, v = p.split("=")
38
+ k = T.cast(k, String)
39
+ [k, v]
40
+ end
41
+ when Router::Verb::POST, Router::Verb::PUT, Router::Verb::PATCH
42
+ # TODO: based on content-type, parse the body differently
43
+ # build-in support for JSON & XML
44
+ body = T.cast(env.fetch("rack.input"), T.any(IO, StringIO))
45
+ res = Oj.load(body.read, Kirei::OJ_OPTIONS)
46
+ body.rewind # TODO: maybe don't rewind if we don't need to?
47
+ T.cast(res, T::Hash[String, T.untyped])
48
+ else
49
+ Logger.logger.warn("Unsupported HTTP verb: #{http_verb.serialize} send to #{req_path}")
50
+ {}
51
+ end
52
+
53
+ req_id = T.cast(env["HTTP_X_REQUEST_ID"], T.nilable(String))
54
+ req_id ||= "req_#{App.environment}_#{SecureRandom.uuid}"
55
+ Thread.current[:request_id] = req_id
56
+
57
+ controller = route.controller
58
+ before_hooks = collect_hooks(controller, :before_hooks)
59
+ run_hooks(before_hooks)
60
+
61
+ status, headers, body = T.cast(
62
+ controller.new(params: params).public_send(route.action),
63
+ RackResponseType,
64
+ )
65
+
66
+ after_hooks = collect_hooks(controller, :after_hooks)
67
+ run_hooks(after_hooks)
68
+
69
+ headers["X-Request-Id"] ||= req_id
70
+
71
+ default_headers.each do |header_name, default_value|
72
+ headers[header_name] ||= default_value
73
+ end
74
+
75
+ [
76
+ status,
77
+ headers,
78
+ body,
79
+ ]
80
+ end
81
+
82
+ #
83
+ # Kirei::App#render
84
+ # * "status": defaults to 200
85
+ # * "headers": defaults to an empty hash
86
+ #
87
+ sig do
88
+ params(
89
+ body: String,
90
+ status: Integer,
91
+ headers: T::Hash[String, String],
92
+ ).returns(RackResponseType)
93
+ end
94
+ def render(body, status: 200, headers: {})
95
+ # merge default headers
96
+ # support a "type" to set content-type header? (or default to json, and users must set the header themselves for other types?)
97
+ [
98
+ status,
99
+ headers,
100
+ [body],
101
+ ]
102
+ end
103
+
104
+ sig { returns(T::Hash[String, String]) }
105
+ def default_headers
106
+ # "Access-Control-Allow-Origin": the user should set that
107
+ {
108
+ # security relevant headers
109
+ "X-Frame-Options" => "DENY",
110
+ "X-Content-Type-Options" => "nosniff",
111
+ "X-XSS-Protection" => "1; mode=block", # for legacy clients/browsers
112
+ "Strict-Transport-Security" => "max-age=31536000; includeSubDomains", # for HTTPS
113
+ "Cache-Control" => "no-store", # the user should set that if caching is needed
114
+ "Referrer-Policy" => "strict-origin-when-cross-origin",
115
+ "Content-Security-Policy" => "default-src 'none'; frame-ancestors 'none'",
116
+
117
+ # other headers
118
+ "Content-Type" => "application/json; charset=utf-8",
119
+ }
120
+ end
121
+
122
+ sig { params(hooks: NilableHooksType).void }
123
+ private def run_hooks(hooks)
124
+ return if hooks.nil? || hooks.empty?
125
+
126
+ hooks.each(&:call)
127
+ end
128
+
129
+ sig do
130
+ params(
131
+ controller: T.class_of(Controller),
132
+ hooks_type: Symbol,
133
+ ).returns(NilableHooksType)
134
+ end
135
+ private def collect_hooks(controller, hooks_type)
136
+ result = T.let(Set.new, T::Set[T.proc.void])
137
+
138
+ controller.ancestors.reverse.each do |ancestor|
139
+ next unless ancestor < Controller
140
+
141
+ supported_hooks = %i[before_hooks after_hooks]
142
+ unless supported_hooks.include?(hooks_type)
143
+ raise "Unexpected hook type, got #{hooks_type}, expected one of: #{supported_hooks.join(",")}"
144
+ end
145
+
146
+ hooks = T.let(ancestor.public_send(hooks_type), NilableHooksType)
147
+ result.merge(hooks) if hooks&.any?
148
+ end
149
+
150
+ result
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ # rubocop:enable Metrics/all
@@ -0,0 +1,10 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ module Routing
6
+ NilableHooksType = T.type_alias do
7
+ T.nilable(T::Set[T.proc.void])
8
+ end
9
+ end
10
+ end
@@ -2,16 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Kirei
5
- module Middleware
6
- # https://github.com/rack/rack/blob/main/UPGRADE-GUIDE.md#rack-3-upgrade-guide
7
- RackResponseType = T.type_alias do
8
- [
9
- Integer,
10
- T::Hash[String, String], # in theory, the values are allowed to be arrays of integers for binary representations
11
- T.any(T::Array[String], Proc),
12
- ]
13
- end
14
-
5
+ module Routing
15
6
  RackEnvType = T.type_alias do
16
7
  T::Hash[
17
8
  String,
@@ -0,0 +1,15 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Kirei
5
+ module Routing
6
+ # https://github.com/rack/rack/blob/main/UPGRADE-GUIDE.md#rack-3-upgrade-guide
7
+ RackResponseType = T.type_alias do
8
+ [
9
+ Integer, # status
10
+ T::Hash[String, String], # headers. Values may be arrays of integers for binary representations
11
+ T.any(T::Array[String], Proc), # body
12
+ ]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,86 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require("singleton")
5
+
6
+ module Kirei
7
+ module Routing
8
+ #
9
+ # Usage:
10
+ #
11
+ # Router.add_routes([
12
+ # Route.new(
13
+ # verb: Kirei::Router::Verb::GET,
14
+ # path: "/livez",
15
+ # controller: Controllers::HealthController,
16
+ # action: "livez",
17
+ # ),
18
+ # ])
19
+ #
20
+ class Router
21
+ extend T::Sig
22
+ include ::Singleton
23
+
24
+ class Verb < T::Enum
25
+ enums do
26
+ # idempotent
27
+ GET = new("GET")
28
+ # non-idempotent
29
+ POST = new("POST")
30
+ # idempotent
31
+ PUT = new("PUT")
32
+ # non-idempotent
33
+ PATCH = new("PATCH")
34
+ # non-idempotent
35
+ DELETE = new("DELETE")
36
+ # idempotent
37
+ HEAD = new("HEAD")
38
+ # idempotent
39
+ OPTIONS = new("OPTIONS")
40
+ # idempotent
41
+ TRACE = new("TRACE")
42
+ # non-idempotent
43
+ CONNECT = new("CONNECT")
44
+ end
45
+ end
46
+
47
+ class Route < T::Struct
48
+ const :verb, Verb
49
+ const :path, String
50
+ const :controller, T.class_of(Controller)
51
+ const :action, String
52
+ end
53
+
54
+ RoutesHash = T.type_alias do
55
+ T::Hash[String, Route]
56
+ end
57
+
58
+ sig { void }
59
+ def initialize
60
+ @routes = T.let({}, RoutesHash)
61
+ end
62
+
63
+ sig { returns(RoutesHash) }
64
+ attr_reader :routes
65
+
66
+ sig do
67
+ params(
68
+ verb: Verb,
69
+ path: String,
70
+ ).returns(T.nilable(Route))
71
+ end
72
+ def get(verb, path)
73
+ key = "#{verb.serialize} #{path}"
74
+ routes[key]
75
+ end
76
+
77
+ sig { params(routes: T::Array[Route]).void }
78
+ def self.add_routes(routes)
79
+ routes.each do |route|
80
+ key = "#{route.verb.serialize} #{route.path}"
81
+ instance.routes[key] = route
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
data/lib/kirei/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Kirei
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.0"
6
6
  end
data/lib/kirei.rb CHANGED
@@ -1,7 +1,30 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "boot"
4
+ # This is the entrypoint into the application,
5
+ # This file loads first, hence we don't have Sorbet loaded yet.
6
+
7
+ #
8
+ # Load Order is important!
9
+ #
10
+
11
+ # First: check if all gems are installed correctly
12
+ require "bundler/setup"
13
+
14
+ # Second: load all gems (runtime dependencies only)
15
+ require "logger"
16
+ require "sorbet-runtime"
17
+ require "oj"
18
+ require "rack"
19
+ require "pg"
20
+ require "sequel" # "sequel_pg" is auto-required by "sequel"
21
+
22
+ # Third: load all application code
23
+ require("zeitwerk")
24
+ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
25
+ loader.ignore("#{__dir__}/cli")
26
+ loader.ignore("#{__dir__}/cli.rb")
27
+ loader.setup
5
28
 
6
29
  module Kirei
7
30
  extend T::Sig
@@ -40,6 +63,8 @@ module Kirei
40
63
  end
41
64
  end
42
65
 
66
+ loader.eager_load
67
+
43
68
  Kirei.configure(&:itself)
44
69
 
45
- Kirei::Logger.logger.info("Kirei (#{Kirei::VERSION}) booted successfully!")
70
+ Kirei::Logger.logger.info("Kirei (v#{Kirei::VERSION}) booted.")
@@ -2,7 +2,7 @@
2
2
 
3
3
  # rubocop:disable Style/EmptyMethod
4
4
  module Kirei
5
- module BaseModel
5
+ module Model
6
6
  include Kernel # "self" is a class since we include the module in a class
7
7
  include T::Props::Serializable
8
8
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kirei
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ludwig Reinmiedl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-09 00:00:00.000000000 Z
11
+ date: 2024-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.5'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.5'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: puma
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -123,9 +137,9 @@ dependencies:
123
137
  - !ruby/object:Gem::Version
124
138
  version: '1.0'
125
139
  description: |
126
- Kirei is a strictly typed Ruby micro/REST-framework for building scaleable and performant microservices.
140
+ Kirei is a Ruby micro/REST-framework for building scalable and performant microservices.
127
141
  It is built from the ground up to be clean and easy to use.
128
- Kirei is based on Sequel as an ORM, Sorbet for typing, and Sinatra for routing.
142
+ It is a Rack app, and uses Sorbet for typing, Sequel as an ORM, Zeitwerk for autoloading, and Puma as a web server.
129
143
  It strives to have zero magic and to be as explicit as possible.
130
144
  email:
131
145
  - lud@reinmiedl.com
@@ -140,7 +154,6 @@ files:
140
154
  - README.md
141
155
  - bin/kirei
142
156
  - kirei.gemspec
143
- - lib/boot.rb
144
157
  - lib/cli.rb
145
158
  - lib/cli/commands/new_app/base_directories.rb
146
159
  - lib/cli/commands/new_app/execute.rb
@@ -154,14 +167,16 @@ files:
154
167
  - lib/cli/commands/start.rb
155
168
  - lib/kirei.rb
156
169
  - lib/kirei/app.rb
157
- - lib/kirei/app_base.rb
158
- - lib/kirei/base_controller.rb
159
- - lib/kirei/base_model.rb
160
170
  - lib/kirei/config.rb
171
+ - lib/kirei/controller.rb
161
172
  - lib/kirei/helpers.rb
162
173
  - lib/kirei/logger.rb
163
- - lib/kirei/middleware.rb
164
- - lib/kirei/router.rb
174
+ - lib/kirei/model.rb
175
+ - lib/kirei/routing/base.rb
176
+ - lib/kirei/routing/nilable_hooks_type.rb
177
+ - lib/kirei/routing/rack_env_type.rb
178
+ - lib/kirei/routing/rack_response_type.rb
179
+ - lib/kirei/routing/router.rb
165
180
  - lib/kirei/version.rb
166
181
  - sorbet/rbi/shims/base_model.rbi
167
182
  homepage: https://github.com/swiknaba/kirei
@@ -185,9 +200,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
200
  - !ruby/object:Gem::Version
186
201
  version: '0'
187
202
  requirements: []
188
- rubygems_version: 3.5.6
203
+ rubygems_version: 3.5.9
189
204
  signing_key:
190
205
  specification_version: 4
191
- summary: Kirei is a strictly typed Ruby micro/REST-framework for building scaleable
192
- and performant microservices.
206
+ summary: Kirei is a typed Ruby micro/REST-framework for building scalable and performant
207
+ microservices.
193
208
  test_files: []
data/lib/boot.rb DELETED
@@ -1,23 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- # This is the entrypoint into the application,
5
- # This file loads first, hence we don't have Sorbet loaded yet.
6
-
7
- #
8
- # Load Order is important!
9
- #
10
-
11
- # First: check if all gems are installed correctly
12
- require "bundler/setup"
13
-
14
- # Second: load all gems (runtime dependencies only)
15
- require "logger"
16
- require "sorbet-runtime"
17
- require "oj"
18
- require "rack"
19
- require "pg"
20
- require "sequel" # "sequel_pg" is auto-required by "sequel"
21
-
22
- # Third: load all application code
23
- Dir[File.join(__dir__, "kirei/**/*.rb")].each { require(_1) }
@@ -1,72 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require_relative("app")
5
-
6
- module Kirei
7
- class AppBase < Kirei::App
8
- class << self
9
- extend T::Sig
10
-
11
- # convenience method since "Kirei.configuration" must be nilable since it is nil
12
- # at the beginning of initilization of the app
13
- sig { returns(Kirei::Config) }
14
- def config
15
- T.must(Kirei.configuration)
16
- end
17
-
18
- sig { returns(Pathname) }
19
- def root
20
- defined?(::APP_ROOT) ? Pathname.new(::APP_ROOT) : Pathname.new(Dir.pwd)
21
- end
22
-
23
- sig { returns(String) }
24
- def version
25
- @version = T.let(@version, T.nilable(String))
26
- @version ||= ENV.fetch("APP_VERSION", nil)
27
- @version ||= ENV.fetch("GIT_SHA", nil)
28
- @version ||= T.must(
29
- `command -v git && git rev-parse --short HEAD`.to_s.split("\n").last,
30
- ).freeze # localhost
31
- end
32
-
33
- sig { returns(String) }
34
- def environment
35
- ENV.fetch("RACK_ENV", "development")
36
- end
37
-
38
- sig { returns(String) }
39
- def default_db_name
40
- @default_db_name ||= T.let("#{config.app_name}_#{environment}".freeze, T.nilable(String))
41
- end
42
-
43
- sig { returns(String) }
44
- def default_db_url
45
- @default_db_url ||= T.let(
46
- ENV.fetch("DATABASE_URL", "postgresql://localhost:5432/#{default_db_name}"),
47
- T.nilable(String),
48
- )
49
- end
50
-
51
- sig { returns(Sequel::Database) }
52
- def raw_db_connection
53
- @raw_db_connection = T.let(@raw_db_connection, T.nilable(Sequel::Database))
54
- return @raw_db_connection unless @raw_db_connection.nil?
55
-
56
- # calling "Sequel.connect" creates a new connection
57
- @raw_db_connection = Sequel.connect(AppBase.config.db_url || default_db_url)
58
-
59
- config.db_extensions.each do |ext|
60
- T.cast(@raw_db_connection, Sequel::Database).extension(ext)
61
- end
62
-
63
- if config.db_extensions.include?(:pg_json)
64
- # https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb#L8
65
- @raw_db_connection.wrap_json_primitives = true
66
- end
67
-
68
- @raw_db_connection
69
- end
70
- end
71
- end
72
- end
@@ -1,16 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require_relative("app")
5
-
6
- module Kirei
7
- class BaseController < Kirei::App
8
- extend T::Sig
9
- # register(Sinatra::Namespace)
10
-
11
- # before do
12
- # Thread.current[:request_id] = request.env["HTTP_X_REQUEST_ID"].presence ||
13
- # "req_#{AppBase.environment}_#{SecureRandom.uuid}"
14
- # end
15
- end
16
- end
data/lib/kirei/router.rb DELETED
@@ -1,61 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require("singleton")
5
-
6
- module Kirei
7
- #
8
- # Usage:
9
- #
10
- # Router.add_routes([
11
- # Route.new(
12
- # verb: "GET",
13
- # path: "/livez",
14
- # controller: Controllers::HealthController,
15
- # action: "livez",
16
- # ),
17
- # ])
18
- #
19
- class Router
20
- extend T::Sig
21
- include ::Singleton
22
-
23
- class Route < T::Struct
24
- const :verb, String
25
- const :path, String
26
- const :controller, T.class_of(BaseController)
27
- const :action, String
28
- end
29
-
30
- RoutesHash = T.type_alias do
31
- T::Hash[String, Route]
32
- end
33
-
34
- sig { void }
35
- def initialize
36
- @routes = T.let({}, RoutesHash)
37
- end
38
-
39
- sig { returns(RoutesHash) }
40
- attr_reader :routes
41
-
42
- sig do
43
- params(
44
- verb: String,
45
- path: String,
46
- ).returns(T.nilable(Route))
47
- end
48
- def get(verb, path)
49
- key = "#{verb} #{path}"
50
- routes[key]
51
- end
52
-
53
- sig { params(routes: T::Array[Route]).void }
54
- def self.add_routes(routes)
55
- routes.each do |route|
56
- key = "#{route.verb} #{route.path}"
57
- instance.routes[key] = route
58
- end
59
- end
60
- end
61
- end