kirei 0.3.0 → 0.4.0
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 +4 -4
- data/README.md +40 -7
- data/kirei.gemspec +1 -0
- data/lib/cli/commands/new_app/execute.rb +1 -1
- data/lib/cli/commands/new_app/files/app.rb +9 -2
- data/lib/cli/commands/new_app/files/routes.rb +2 -2
- data/lib/cli/commands/start.rb +1 -1
- data/lib/kirei/app.rb +3 -0
- data/lib/kirei/config.rb +4 -1
- data/lib/kirei/errors/json_api_error.rb +25 -0
- data/lib/kirei/errors/json_api_error_source.rb +12 -0
- data/lib/kirei/logging/level.rb +33 -0
- data/lib/kirei/logging/logger.rb +198 -0
- data/lib/kirei/logging/metric.rb +40 -0
- data/lib/kirei/model/base_class_interface.rb +55 -0
- data/lib/kirei/model/class_methods.rb +157 -0
- data/lib/kirei/model/human_id_generator.rb +40 -0
- data/lib/kirei/model.rb +4 -175
- data/lib/kirei/routing/base.rb +43 -12
- data/lib/kirei/routing/route.rb +13 -0
- data/lib/kirei/routing/router.rb +1 -31
- data/lib/kirei/routing/verb.rb +37 -0
- data/lib/kirei/services/result.rb +53 -0
- data/lib/kirei/services/runner.rb +47 -0
- data/lib/kirei/version.rb +1 -1
- data/lib/kirei.rb +5 -2
- data/sorbet/rbi/shims/ruby.rbi +15 -0
- metadata +29 -3
- data/lib/kirei/logger.rb +0 -196
@@ -0,0 +1,157 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
module Model
|
6
|
+
module ClassMethods
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Generic
|
9
|
+
|
10
|
+
# the attached class is the class that extends this module
|
11
|
+
# e.g. "User", "Airport", ..
|
12
|
+
has_attached_class!
|
13
|
+
|
14
|
+
include Kirei::Model::BaseClassInterface
|
15
|
+
|
16
|
+
# defaults to a pluralized, underscored version of the class name
|
17
|
+
sig { override.returns(String) }
|
18
|
+
def table_name
|
19
|
+
@table_name ||= T.let("#{model_name}s", T.nilable(String))
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { returns(String) }
|
23
|
+
def model_name
|
24
|
+
Kirei::Helpers.underscore(T.must(name.split("::").last))
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { override.returns(Sequel::Dataset) }
|
28
|
+
def query
|
29
|
+
db[table_name.to_sym]
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { override.returns(Sequel::Database) }
|
33
|
+
def db
|
34
|
+
App.raw_db_connection
|
35
|
+
end
|
36
|
+
|
37
|
+
sig do
|
38
|
+
override.params(
|
39
|
+
hash: T::Hash[Symbol, T.untyped],
|
40
|
+
).returns(T::Array[T.attached_class])
|
41
|
+
end
|
42
|
+
def where(hash)
|
43
|
+
resolve(query.where(hash))
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { override.returns(T::Array[T.attached_class]) }
|
47
|
+
def all
|
48
|
+
resolve(query.all)
|
49
|
+
end
|
50
|
+
|
51
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
52
|
+
# default values defined in the model are used, if omitted in the hash
|
53
|
+
sig do
|
54
|
+
override.params(
|
55
|
+
hash: T::Hash[Symbol, T.untyped],
|
56
|
+
).returns(T.attached_class)
|
57
|
+
end
|
58
|
+
def create(hash)
|
59
|
+
# instantiate a new object to ensure we use default values defined in the model
|
60
|
+
without_id = !hash.key?(:id)
|
61
|
+
hash[:id] = "kirei-fake-id" if without_id
|
62
|
+
new_record = from_hash(Helpers.deep_stringify_keys(hash))
|
63
|
+
all_attributes = T.let(new_record.serialize, T::Hash[String, T.untyped])
|
64
|
+
all_attributes.delete("id") if without_id && all_attributes["id"] == "kirei-fake-id"
|
65
|
+
|
66
|
+
wrap_jsonb_non_primivitives!(all_attributes)
|
67
|
+
|
68
|
+
if new_record.respond_to?(:created_at) && all_attributes["created_at"].nil?
|
69
|
+
all_attributes["created_at"] = Time.now.utc
|
70
|
+
end
|
71
|
+
if new_record.respond_to?(:updated_at) && all_attributes["updated_at"].nil?
|
72
|
+
all_attributes["updated_at"] = Time.now.utc
|
73
|
+
end
|
74
|
+
|
75
|
+
pkey = T.let(query.insert(all_attributes), String)
|
76
|
+
|
77
|
+
T.must(find_by({ id: pkey }))
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
80
|
+
|
81
|
+
sig { override.params(attributes: T::Hash[T.any(Symbol, String), T.untyped]).void }
|
82
|
+
def wrap_jsonb_non_primivitives!(attributes)
|
83
|
+
# setting `@raw_db_connection.wrap_json_primitives = true`
|
84
|
+
# only works on JSON primitives, but not on blank hashes/arrays
|
85
|
+
return unless App.config.db_extensions.include?(:pg_json)
|
86
|
+
|
87
|
+
attributes.each_pair do |key, value|
|
88
|
+
next unless value.is_a?(Hash) || value.is_a?(Array)
|
89
|
+
|
90
|
+
attributes[key] = T.unsafe(Sequel).pg_jsonb_wrap(value)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
sig do
|
95
|
+
override.params(
|
96
|
+
hash: T::Hash[Symbol, T.untyped],
|
97
|
+
).returns(T.nilable(T.attached_class))
|
98
|
+
end
|
99
|
+
def find_by(hash)
|
100
|
+
resolve_first(query.where(hash))
|
101
|
+
end
|
102
|
+
|
103
|
+
# Extra or unknown properties present in the Hash do not raise exceptions at
|
104
|
+
# runtime unless the optional strict argument to from_hash is passed
|
105
|
+
#
|
106
|
+
# Source: https://sorbet.org/docs/tstruct#from_hash-gotchas
|
107
|
+
# "strict" defaults to "false".
|
108
|
+
sig do
|
109
|
+
override.params(
|
110
|
+
query: T.any(Sequel::Dataset, T::Array[T::Hash[Symbol, T.untyped]]),
|
111
|
+
strict: T.nilable(T::Boolean),
|
112
|
+
).returns(T::Array[T.attached_class])
|
113
|
+
end
|
114
|
+
def resolve(query, strict = nil)
|
115
|
+
strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
|
116
|
+
|
117
|
+
query.map do |row|
|
118
|
+
row = T.cast(row, T::Hash[Symbol, T.untyped])
|
119
|
+
row.transform_keys!(&:to_s) # sequel returns symbolized keys
|
120
|
+
from_hash(row, strict_loading)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
sig do
|
125
|
+
override.params(
|
126
|
+
query: Sequel::Dataset,
|
127
|
+
strict: T.nilable(T::Boolean),
|
128
|
+
).returns(T.nilable(T.attached_class))
|
129
|
+
end
|
130
|
+
def resolve_first(query, strict = nil)
|
131
|
+
strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
|
132
|
+
|
133
|
+
resolve(query.limit(1), strict_loading).first
|
134
|
+
end
|
135
|
+
|
136
|
+
# defaults to 6
|
137
|
+
sig { override.returns(Integer) }
|
138
|
+
def human_id_length = 6
|
139
|
+
|
140
|
+
# defaults to "model_name" (table_name without the trailing "s")
|
141
|
+
sig { override.returns(String) }
|
142
|
+
def human_id_prefix = model_name
|
143
|
+
|
144
|
+
#
|
145
|
+
# Generates a human-readable ID for the record.
|
146
|
+
# The ID is prefixed with the table name and an underscore.
|
147
|
+
#
|
148
|
+
sig { override.returns(String) }
|
149
|
+
def generate_human_id
|
150
|
+
Kirei::Model::HumanIdGenerator.call(
|
151
|
+
length: human_id_length,
|
152
|
+
prefix: human_id_prefix,
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
module Model
|
6
|
+
class HumanIdGenerator
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
# Removed ambiguous characters 0, 1, O, I, l, 5, S
|
10
|
+
ALLOWED_CHARS = "2346789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrtuvwxyz"
|
11
|
+
private_constant :ALLOWED_CHARS
|
12
|
+
|
13
|
+
ALLOWED_CHARS_COUNT = T.let(ALLOWED_CHARS.size, Integer)
|
14
|
+
private_constant :ALLOWED_CHARS_COUNT
|
15
|
+
|
16
|
+
sig do
|
17
|
+
params(
|
18
|
+
length: Integer,
|
19
|
+
prefix: String,
|
20
|
+
).returns(String)
|
21
|
+
end
|
22
|
+
def self.call(length:, prefix:)
|
23
|
+
"#{prefix}_#{random_id(length)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { params(key_length: Integer).returns(String) }
|
27
|
+
private_class_method def self.random_id(key_length)
|
28
|
+
random_chars = Array.new(key_length)
|
29
|
+
|
30
|
+
key_length.times do |idx|
|
31
|
+
random_char_idx = SecureRandom.random_number(ALLOWED_CHARS_COUNT)
|
32
|
+
random_char = T.must(ALLOWED_CHARS[random_char_idx])
|
33
|
+
random_chars[idx] = random_char
|
34
|
+
end
|
35
|
+
|
36
|
+
random_chars.join
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/kirei/model.rb
CHANGED
@@ -6,7 +6,7 @@ module Kirei
|
|
6
6
|
extend T::Sig
|
7
7
|
extend T::Helpers
|
8
8
|
|
9
|
-
sig { returns(BaseClassInterface) }
|
9
|
+
sig { returns(Kirei::Model::BaseClassInterface) }
|
10
10
|
def class; super; end # rubocop:disable all
|
11
11
|
|
12
12
|
# An update keeps the original object intact, and returns a new object with the updated values.
|
@@ -18,7 +18,7 @@ module Kirei
|
|
18
18
|
def update(hash)
|
19
19
|
hash[:updated_at] = Time.now.utc if respond_to?(:updated_at) && hash[:updated_at].nil?
|
20
20
|
self.class.wrap_jsonb_non_primivitives!(hash)
|
21
|
-
self.class.
|
21
|
+
self.class.query.where({ id: id }).update(hash)
|
22
22
|
self.class.find_by({ id: id })
|
23
23
|
end
|
24
24
|
|
@@ -26,7 +26,7 @@ module Kirei
|
|
26
26
|
# Calling delete multiple times will return false after the first (successful) call.
|
27
27
|
sig { returns(T::Boolean) }
|
28
28
|
def delete
|
29
|
-
count = self.class.
|
29
|
+
count = self.class.query.where({ id: id }).delete
|
30
30
|
count == 1
|
31
31
|
end
|
32
32
|
|
@@ -47,177 +47,6 @@ module Kirei
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
|
51
|
-
extend T::Sig
|
52
|
-
extend T::Helpers
|
53
|
-
interface!
|
54
|
-
|
55
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
56
|
-
def find_by(hash)
|
57
|
-
end
|
58
|
-
|
59
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
60
|
-
def where(hash)
|
61
|
-
end
|
62
|
-
|
63
|
-
sig { abstract.returns(T.untyped) }
|
64
|
-
def all
|
65
|
-
end
|
66
|
-
|
67
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
68
|
-
def create(hash)
|
69
|
-
end
|
70
|
-
|
71
|
-
sig { abstract.params(attributes: T.untyped).void }
|
72
|
-
def wrap_jsonb_non_primivitives!(attributes)
|
73
|
-
end
|
74
|
-
|
75
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
76
|
-
def resolve(hash)
|
77
|
-
end
|
78
|
-
|
79
|
-
sig { abstract.params(hash: T.untyped).returns(T.untyped) }
|
80
|
-
def resolve_first(hash)
|
81
|
-
end
|
82
|
-
|
83
|
-
sig { abstract.returns(T.untyped) }
|
84
|
-
def table_name
|
85
|
-
end
|
86
|
-
|
87
|
-
sig { abstract.returns(T.untyped) }
|
88
|
-
def db
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
module ClassMethods
|
93
|
-
extend T::Sig
|
94
|
-
extend T::Generic
|
95
|
-
|
96
|
-
# the attached class is the class that extends this module
|
97
|
-
# e.g. "User"
|
98
|
-
# extend T::Generic
|
99
|
-
# has_attached_class!
|
100
|
-
has_attached_class!
|
101
|
-
|
102
|
-
include BaseClassInterface
|
103
|
-
|
104
|
-
# defaults to a pluralized, underscored version of the class name
|
105
|
-
sig { override.returns(String) }
|
106
|
-
def table_name
|
107
|
-
@table_name ||= T.let(
|
108
|
-
begin
|
109
|
-
table_name_ = Kirei::Helpers.underscore(T.must(name.split("::").last))
|
110
|
-
"#{table_name_}s"
|
111
|
-
end,
|
112
|
-
T.nilable(String),
|
113
|
-
)
|
114
|
-
end
|
115
|
-
|
116
|
-
sig { override.returns(Sequel::Dataset) }
|
117
|
-
def db
|
118
|
-
App.raw_db_connection[table_name.to_sym]
|
119
|
-
end
|
120
|
-
|
121
|
-
sig do
|
122
|
-
override.params(
|
123
|
-
hash: T::Hash[Symbol, T.untyped],
|
124
|
-
).returns(T::Array[T.attached_class])
|
125
|
-
end
|
126
|
-
def where(hash)
|
127
|
-
resolve(db.where(hash))
|
128
|
-
end
|
129
|
-
|
130
|
-
sig { override.returns(T::Array[T.attached_class]) }
|
131
|
-
def all
|
132
|
-
resolve(db.all)
|
133
|
-
end
|
134
|
-
|
135
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
136
|
-
# default values defined in the model are used, if omitted in the hash
|
137
|
-
sig do
|
138
|
-
override.params(
|
139
|
-
hash: T::Hash[Symbol, T.untyped],
|
140
|
-
).returns(T.attached_class)
|
141
|
-
end
|
142
|
-
def create(hash)
|
143
|
-
# instantiate a new object to ensure we use default values defined in the model
|
144
|
-
without_id = !hash.key?(:id)
|
145
|
-
hash[:id] = "kirei-fake-id" if without_id
|
146
|
-
new_record = from_hash(Helpers.deep_stringify_keys(hash))
|
147
|
-
all_attributes = T.let(new_record.serialize, T::Hash[String, T.untyped])
|
148
|
-
all_attributes.delete("id") if without_id && all_attributes["id"] == "kirei-fake-id"
|
149
|
-
|
150
|
-
wrap_jsonb_non_primivitives!(all_attributes)
|
151
|
-
|
152
|
-
if new_record.respond_to?(:created_at) && all_attributes["created_at"].nil?
|
153
|
-
all_attributes["created_at"] = Time.now.utc
|
154
|
-
end
|
155
|
-
if new_record.respond_to?(:updated_at) && all_attributes["updated_at"].nil?
|
156
|
-
all_attributes["updated_at"] = Time.now.utc
|
157
|
-
end
|
158
|
-
|
159
|
-
pkey = T.let(db.insert(all_attributes), String)
|
160
|
-
|
161
|
-
T.must(find_by({ id: pkey }))
|
162
|
-
end
|
163
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
164
|
-
|
165
|
-
sig { override.params(attributes: T::Hash[T.any(Symbol, String), T.untyped]).void }
|
166
|
-
def wrap_jsonb_non_primivitives!(attributes)
|
167
|
-
# setting `@raw_db_connection.wrap_json_primitives = true`
|
168
|
-
# only works on JSON primitives, but not on blank hashes/arrays
|
169
|
-
return unless App.config.db_extensions.include?(:pg_json)
|
170
|
-
|
171
|
-
attributes.each_pair do |key, value|
|
172
|
-
next unless value.is_a?(Hash) || value.is_a?(Array)
|
173
|
-
|
174
|
-
attributes[key] = T.unsafe(Sequel).pg_jsonb_wrap(value)
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
sig do
|
179
|
-
override.params(
|
180
|
-
hash: T::Hash[Symbol, T.untyped],
|
181
|
-
).returns(T.nilable(T.attached_class))
|
182
|
-
end
|
183
|
-
def find_by(hash)
|
184
|
-
resolve_first(db.where(hash))
|
185
|
-
end
|
186
|
-
|
187
|
-
# Extra or unknown properties present in the Hash do not raise exceptions at
|
188
|
-
# runtime unless the optional strict argument to from_hash is passed
|
189
|
-
#
|
190
|
-
# Source: https://sorbet.org/docs/tstruct#from_hash-gotchas
|
191
|
-
# "strict" defaults to "false".
|
192
|
-
sig do
|
193
|
-
override.params(
|
194
|
-
query: T.any(Sequel::Dataset, T::Array[T::Hash[Symbol, T.untyped]]),
|
195
|
-
strict: T.nilable(T::Boolean),
|
196
|
-
).returns(T::Array[T.attached_class])
|
197
|
-
end
|
198
|
-
def resolve(query, strict = nil)
|
199
|
-
strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
|
200
|
-
|
201
|
-
query.map do |row|
|
202
|
-
row = T.cast(row, T::Hash[Symbol, T.untyped])
|
203
|
-
row.transform_keys!(&:to_s) # sequel returns symbolized keys
|
204
|
-
from_hash(row, strict_loading)
|
205
|
-
end
|
206
|
-
end
|
207
|
-
|
208
|
-
sig do
|
209
|
-
override.params(
|
210
|
-
query: Sequel::Dataset,
|
211
|
-
strict: T.nilable(T::Boolean),
|
212
|
-
).returns(T.nilable(T.attached_class))
|
213
|
-
end
|
214
|
-
def resolve_first(query, strict = nil)
|
215
|
-
strict_loading = strict.nil? ? App.config.db_strict_type_resolving : strict
|
216
|
-
|
217
|
-
resolve(query.limit(1), strict_loading).first
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
mixes_in_class_methods(ClassMethods)
|
50
|
+
mixes_in_class_methods(Kirei::Model::ClassMethods)
|
222
51
|
end
|
223
52
|
end
|
data/lib/kirei/routing/base.rb
CHANGED
@@ -17,9 +17,14 @@ module Kirei
|
|
17
17
|
sig { returns(T::Hash[String, T.untyped]) }
|
18
18
|
attr_reader :params
|
19
19
|
|
20
|
+
sig { returns(Router) }
|
21
|
+
attr_reader :router; private :router
|
22
|
+
|
20
23
|
sig { params(env: RackEnvType).returns(RackResponseType) }
|
21
24
|
def call(env)
|
22
|
-
|
25
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
26
|
+
|
27
|
+
http_verb = Verb.deserialize(env.fetch("REQUEST_METHOD"))
|
23
28
|
req_path = T.cast(env.fetch("REQUEST_PATH"), String)
|
24
29
|
#
|
25
30
|
# TODO: reject requests from unexpected hosts -> allow configuring allowed hosts in a `cors.rb` file
|
@@ -27,18 +32,18 @@ module Kirei
|
|
27
32
|
# -> use https://github.com/cyu/rack-cors ?
|
28
33
|
#
|
29
34
|
|
30
|
-
route =
|
35
|
+
route = router.get(http_verb, req_path)
|
31
36
|
return [404, {}, ["Not Found"]] if route.nil?
|
32
37
|
|
33
38
|
params = case route.verb
|
34
|
-
when
|
39
|
+
when Verb::GET
|
35
40
|
query = T.cast(env.fetch("QUERY_STRING"), String)
|
36
41
|
query.split("&").to_h do |p|
|
37
42
|
k, v = p.split("=")
|
38
43
|
k = T.cast(k, String)
|
39
44
|
[k, v]
|
40
45
|
end
|
41
|
-
when
|
46
|
+
when Verb::POST, Verb::PUT, Verb::PATCH
|
42
47
|
# TODO: based on content-type, parse the body differently
|
43
48
|
# build-in support for JSON & XML
|
44
49
|
body = T.cast(env.fetch("rack.input"), T.any(IO, StringIO))
|
@@ -46,7 +51,7 @@ module Kirei
|
|
46
51
|
body.rewind # TODO: maybe don't rewind if we don't need to?
|
47
52
|
T.cast(res, T::Hash[String, T.untyped])
|
48
53
|
else
|
49
|
-
Logger.logger.warn("Unsupported HTTP verb: #{http_verb.serialize} send to #{req_path}")
|
54
|
+
Logging::Logger.logger.warn("Unsupported HTTP verb: #{http_verb.serialize} send to #{req_path}")
|
50
55
|
{}
|
51
56
|
end
|
52
57
|
|
@@ -58,7 +63,19 @@ module Kirei
|
|
58
63
|
before_hooks = collect_hooks(controller, :before_hooks)
|
59
64
|
run_hooks(before_hooks)
|
60
65
|
|
61
|
-
|
66
|
+
Kirei::Logging::Logger.call(
|
67
|
+
level: Kirei::Logging::Level::INFO,
|
68
|
+
label: "Request Started",
|
69
|
+
meta: params,
|
70
|
+
)
|
71
|
+
|
72
|
+
statsd_timing_tags = {
|
73
|
+
"controller" => controller.name,
|
74
|
+
"route" => route.action,
|
75
|
+
}
|
76
|
+
Logging::Metric.inject_defaults(statsd_timing_tags)
|
77
|
+
|
78
|
+
status, headers, response_body = T.cast(
|
62
79
|
controller.new(params: params).public_send(route.action),
|
63
80
|
RackResponseType,
|
64
81
|
)
|
@@ -75,14 +92,30 @@ module Kirei
|
|
75
92
|
[
|
76
93
|
status,
|
77
94
|
headers,
|
78
|
-
|
95
|
+
response_body,
|
79
96
|
]
|
97
|
+
ensure
|
98
|
+
stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
99
|
+
if start # early return for 404
|
100
|
+
latency_in_ms = stop - start
|
101
|
+
::StatsD.measure("request", latency_in_ms, tags: statsd_timing_tags)
|
102
|
+
|
103
|
+
Kirei::Logging::Logger.call(
|
104
|
+
level: Kirei::Logging::Level::INFO,
|
105
|
+
label: "Request Finished",
|
106
|
+
meta: { "response.body" => response_body, "response.latency_in_ms" => latency_in_ms },
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
# reset global variables after the request has been served
|
111
|
+
# and after all "after" hooks have run to avoid leaking
|
112
|
+
Thread.current[:enduser_id] = nil
|
113
|
+
Thread.current[:request_id] = nil
|
80
114
|
end
|
81
115
|
|
82
116
|
#
|
83
|
-
# Kirei::App#render
|
84
117
|
# * "status": defaults to 200
|
85
|
-
# * "headers":
|
118
|
+
# * "headers": Kirei adds some default headers for security, but the user can override them
|
86
119
|
#
|
87
120
|
sig do
|
88
121
|
params(
|
@@ -92,8 +125,6 @@ module Kirei
|
|
92
125
|
).returns(RackResponseType)
|
93
126
|
end
|
94
127
|
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
128
|
[
|
98
129
|
status,
|
99
130
|
headers,
|
@@ -103,7 +134,7 @@ module Kirei
|
|
103
134
|
|
104
135
|
sig { returns(T::Hash[String, String]) }
|
105
136
|
def default_headers
|
106
|
-
# "Access-Control-Allow-Origin": the user should set that
|
137
|
+
# "Access-Control-Allow-Origin": the user should set that, see comment about "cors" above
|
107
138
|
{
|
108
139
|
# security relevant headers
|
109
140
|
"X-Frame-Options" => "DENY",
|
data/lib/kirei/routing/router.rb
CHANGED
@@ -10,7 +10,7 @@ module Kirei
|
|
10
10
|
#
|
11
11
|
# Router.add_routes([
|
12
12
|
# Route.new(
|
13
|
-
# verb:
|
13
|
+
# verb: Verb::GET,
|
14
14
|
# path: "/livez",
|
15
15
|
# controller: Controllers::HealthController,
|
16
16
|
# action: "livez",
|
@@ -21,36 +21,6 @@ module Kirei
|
|
21
21
|
extend T::Sig
|
22
22
|
include ::Singleton
|
23
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
24
|
RoutesHash = T.type_alias do
|
55
25
|
T::Hash[String, Route]
|
56
26
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
module Routing
|
6
|
+
class Verb < T::Enum
|
7
|
+
enums do
|
8
|
+
# idempotent
|
9
|
+
GET = new("GET")
|
10
|
+
|
11
|
+
# non-idempotent
|
12
|
+
POST = new("POST")
|
13
|
+
|
14
|
+
# idempotent
|
15
|
+
PUT = new("PUT")
|
16
|
+
|
17
|
+
# non-idempotent
|
18
|
+
PATCH = new("PATCH")
|
19
|
+
|
20
|
+
# non-idempotent
|
21
|
+
DELETE = new("DELETE")
|
22
|
+
|
23
|
+
# idempotent
|
24
|
+
HEAD = new("HEAD")
|
25
|
+
|
26
|
+
# idempotent
|
27
|
+
OPTIONS = new("OPTIONS")
|
28
|
+
|
29
|
+
# idempotent
|
30
|
+
TRACE = new("TRACE")
|
31
|
+
|
32
|
+
# non-idempotent
|
33
|
+
CONNECT = new("CONNECT")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Kirei
|
5
|
+
module Services
|
6
|
+
class Result
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Generic
|
9
|
+
|
10
|
+
ErrorType = type_member { { fixed: T::Array[Errors::JsonApiError] } }
|
11
|
+
ResultType = type_member { { upper: Object } }
|
12
|
+
|
13
|
+
sig do
|
14
|
+
params(
|
15
|
+
result: T.nilable(ResultType),
|
16
|
+
errors: ErrorType,
|
17
|
+
).void
|
18
|
+
end
|
19
|
+
def initialize(result: nil, errors: [])
|
20
|
+
if (result && !errors.empty?) || (!result && errors.empty?)
|
21
|
+
raise ArgumentError, "Must provide either result or errors, got both or neither"
|
22
|
+
end
|
23
|
+
|
24
|
+
@result = result
|
25
|
+
@errors = errors
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { returns(T::Boolean) }
|
29
|
+
def success?
|
30
|
+
@errors.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { returns(T::Boolean) }
|
34
|
+
def failed?
|
35
|
+
!success?
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { returns(ResultType) }
|
39
|
+
def result
|
40
|
+
raise "Cannot call 'result' when there are errors" if failed?
|
41
|
+
|
42
|
+
T.must(@result)
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { returns(ErrorType) }
|
46
|
+
def errors
|
47
|
+
raise "Cannot call 'errors' when there is a result" if success?
|
48
|
+
|
49
|
+
@errors
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|