kirei 0.8.4 → 0.9.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 +75 -7
- data/cops/layout.yml +17 -0
- data/cops/lint.yml +23 -0
- data/cops/metrics.yml +20 -0
- data/cops/naming.yml +2 -0
- data/cops/rspec.yml +13 -0
- data/cops/style.yml +77 -0
- data/cops/types.yml +12 -0
- data/kirei.gemspec +1 -1
- data/lib/kirei/config.rb +1 -0
- data/lib/kirei/domain/value_object.rb +0 -18
- data/lib/kirei/helpers.rb +2 -8
- data/lib/kirei/logging/logger.rb +1 -1
- data/lib/kirei/logging/metric.rb +1 -3
- data/lib/kirei/metrics/backend.rb +43 -0
- data/lib/kirei/metrics/logging_backend.rb +43 -0
- data/lib/kirei/metrics/null_backend.rb +40 -0
- data/lib/kirei/metrics/statsd_backend.rb +51 -0
- data/lib/kirei/model/base_class_interface.rb +1 -0
- data/lib/kirei/model/class_methods.rb +8 -2
- data/lib/kirei/model.rb +1 -1
- data/lib/kirei/routing/base.rb +59 -7
- data/lib/kirei/routing/route.rb +12 -0
- data/lib/kirei/routing/router.rb +72 -2
- data/lib/kirei/services/runner.rb +1 -1
- data/lib/kirei/version.rb +1 -1
- data/lib/kirei.rb +0 -1
- data/sorbet/rbi/shims/statsd.rbi +17 -0
- metadata +13 -16
- data/lib/kirei/services/array_comparison.rb +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e175975ffe2d5eef5e50f3125cdf29f3c56005458362c1c45080c2cc54642e36
|
|
4
|
+
data.tar.gz: e9b84d4fb46d281d2f601515c96d186daf64a89e95661b48cf288c088255f420
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7fc9d9756a1f4a7b2e030063f1aad7378dd054e7312ea0d04222fa5537278fae1a29a77e760be80d22507c5c2a054e8bc63743471f9991c60c505f09919b3979
|
|
7
|
+
data.tar.gz: 68a0e0a944c7596f682e48beae0eaade2ee046b1110de7c1f50ea034b035bb0d386347a82117207624d90ee03b267796102a24fb10ece7e2458d2784b95d1629
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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 [Rack](https://github.com/rack/rack) as web server interface. It strives to have zero magic and to be as explicit as possible.
|
|
4
4
|
|
|
5
|
-
Kirei's main advantages over other frameworks are its strict typing, low memory footprint, and
|
|
5
|
+
Kirei's main advantages over other frameworks are its strict typing, low memory footprint, and built-in high-performance logging and pluggable metric-tracking toolkit. It is opinionated 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
6
|
|
|
7
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
8
|
|
|
@@ -227,6 +227,12 @@ module Kirei::Routing
|
|
|
227
227
|
controller: Controllers::Airports,
|
|
228
228
|
action: "index",
|
|
229
229
|
),
|
|
230
|
+
Route.new(
|
|
231
|
+
verb: Verb::GET,
|
|
232
|
+
path: "/airports/:iata",
|
|
233
|
+
controller: Controllers::Airports,
|
|
234
|
+
action: "show",
|
|
235
|
+
),
|
|
230
236
|
],
|
|
231
237
|
)
|
|
232
238
|
end
|
|
@@ -234,7 +240,24 @@ end
|
|
|
234
240
|
|
|
235
241
|
#### Controllers
|
|
236
242
|
|
|
237
|
-
Controllers can be defined anywhere; by convention, they are defined in the `app/controllers` directory
|
|
243
|
+
Controllers can be defined anywhere; by convention, they are defined in the `app/controllers` directory.
|
|
244
|
+
|
|
245
|
+
Three render helpers are available:
|
|
246
|
+
|
|
247
|
+
| Method | Use case |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `render(body, status:, headers:)` | Raw string responses (plain text, pre-serialized data) |
|
|
250
|
+
| `render_json(data, status:, headers:)` | JSON responses with automatic serialization |
|
|
251
|
+
| `render_error(errors, status:, headers:)` | JSON:API-compliant error responses |
|
|
252
|
+
|
|
253
|
+
`render_json` accepts multiple data types:
|
|
254
|
+
|
|
255
|
+
| `data` type | Behavior |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `String` | Pass-through (assumed to be pre-serialized JSON) |
|
|
258
|
+
| `Hash` / `Array` | Serialized via `Oj.dump` |
|
|
259
|
+
| Object responding to `#serialize` (e.g. `T::Struct`) | Calls `.serialize`, then `Oj.dump` if the result is not a String |
|
|
260
|
+
| Anything else | Raises `ArgumentError` |
|
|
238
261
|
|
|
239
262
|
```ruby
|
|
240
263
|
module Controllers
|
|
@@ -245,14 +268,24 @@ module Controllers
|
|
|
245
268
|
def index
|
|
246
269
|
search = T.let(params.fetch("q", nil), T.nilable(String))
|
|
247
270
|
|
|
248
|
-
|
|
249
|
-
Airports::Filter.call(search)
|
|
271
|
+
service = Kirei::Services::Runner.call("Airports::Filter") do
|
|
272
|
+
Airports::Filter.call(search)
|
|
250
273
|
end
|
|
274
|
+
return render_error(service.errors, status: 400) if service.failed?
|
|
251
275
|
|
|
252
|
-
|
|
253
|
-
|
|
276
|
+
render_json(service.result.map(&:serialize))
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
sig { returns(T.anything) }
|
|
280
|
+
def show
|
|
281
|
+
iata = T.must(params.fetch("iata", nil)) # named param from dynamic route
|
|
254
282
|
|
|
255
|
-
|
|
283
|
+
airport = Kirei::Services::Runner.call("Airports::Find") do
|
|
284
|
+
Airports::Find.call(iata) # T.nilable(Airport)
|
|
285
|
+
end
|
|
286
|
+
return render(status: 204) if airport.nil?
|
|
287
|
+
|
|
288
|
+
render_json(airport) # T::Struct — calls .serialize automatically
|
|
256
289
|
end
|
|
257
290
|
end
|
|
258
291
|
end
|
|
@@ -287,11 +320,46 @@ module Airports
|
|
|
287
320
|
end
|
|
288
321
|
```
|
|
289
322
|
|
|
323
|
+
#### Metrics
|
|
324
|
+
|
|
325
|
+
Kirei ships with a pluggable metrics interface via `Kirei::Metrics::Backend`. Three backends are included:
|
|
326
|
+
|
|
327
|
+
| Backend | Description |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `LoggingBackend` | **Default.** Prints metrics to stdout via `puts` — great for local development and small MVPs. |
|
|
330
|
+
| `StatsdBackend` | Wraps [`statsd-instrument`](https://github.com/Shopify/statsd-instrument). Add `gem 'statsd-instrument'` to your Gemfile. |
|
|
331
|
+
| `NullBackend` | No-op — silently discards all metrics. |
|
|
332
|
+
|
|
333
|
+
The backend exposes three methods: `increment`, `measure`, and `gauge`.
|
|
334
|
+
|
|
335
|
+
Configure the backend in your app:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
class MyApp < Kirei::App
|
|
339
|
+
# Use StatsD (requires `gem 'statsd-instrument'` in Gemfile)
|
|
340
|
+
config.metrics_backend = Kirei::Metrics::StatsdBackend.new
|
|
341
|
+
|
|
342
|
+
# Or disable metrics entirely
|
|
343
|
+
config.metrics_backend = Kirei::Metrics::NullBackend.new
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Emit custom metrics anywhere via `Kirei::Logging::Metric`:
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
Kirei::Logging::Metric.call("airports_search_term", 1, tags: { "query" => search })
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Request timing and service execution timing are tracked automatically.
|
|
354
|
+
|
|
355
|
+
To build a custom backend (e.g. Prometheus, OpenTelemetry), subclass `Kirei::Metrics::Backend` and implement `increment`, `measure`, and `gauge`.
|
|
356
|
+
|
|
290
357
|
### Goes well with these gems
|
|
291
358
|
|
|
292
359
|
* [pagy](https://github.com/ddnexus/pagy) for pagination
|
|
293
360
|
* [argon2](https://github.com/technion/ruby-argon2) for password hashing
|
|
294
361
|
* [rack-session](https://github.com/rack/rack-session) for session management
|
|
362
|
+
* [pgvector](https://github.com/pgvector/pgvector-ruby) for vector columns — add `:pgvector` to `App.config.db_extensions`
|
|
295
363
|
|
|
296
364
|
### Middlewares
|
|
297
365
|
|
data/cops/layout.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Layout/EndAlignment:
|
|
2
|
+
Enabled: true
|
|
3
|
+
EnforcedStyleAlignWith: variable
|
|
4
|
+
AutoCorrect: true
|
|
5
|
+
|
|
6
|
+
Layout/ParameterAlignment:
|
|
7
|
+
EnforcedStyle: with_fixed_indentation
|
|
8
|
+
SupportedStyles:
|
|
9
|
+
- with_first_parameter
|
|
10
|
+
- with_fixed_indentation
|
|
11
|
+
|
|
12
|
+
Layout/LineLength:
|
|
13
|
+
Max: 120
|
|
14
|
+
AllowCopDirectives: false
|
|
15
|
+
AllowedPatterns: ['\s#\s|^#\s'] # allows comments to overflow the line length. E.g. for URLs
|
|
16
|
+
Exclude:
|
|
17
|
+
- "spec/**/*"
|
data/cops/lint.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Lint/DuplicateBranch:
|
|
2
|
+
Enabled: true
|
|
3
|
+
|
|
4
|
+
Lint/DuplicateRegexpCharacterClassElement:
|
|
5
|
+
Enabled: true
|
|
6
|
+
|
|
7
|
+
Lint/EmptyBlock:
|
|
8
|
+
Enabled: true
|
|
9
|
+
|
|
10
|
+
Lint/EmptyClass:
|
|
11
|
+
Enabled: true
|
|
12
|
+
|
|
13
|
+
Lint/NoReturnInBeginEndBlocks:
|
|
14
|
+
Enabled: true
|
|
15
|
+
|
|
16
|
+
Lint/ToEnumArguments:
|
|
17
|
+
Enabled: true
|
|
18
|
+
|
|
19
|
+
Lint/UnexpectedBlockArity:
|
|
20
|
+
Enabled: true
|
|
21
|
+
|
|
22
|
+
Lint/UnmodifiedReduceAccumulator:
|
|
23
|
+
Enabled: true
|
data/cops/metrics.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metrics/BlockLength:
|
|
2
|
+
Exclude:
|
|
3
|
+
- describe
|
|
4
|
+
- context
|
|
5
|
+
- feature
|
|
6
|
+
- scenario
|
|
7
|
+
|
|
8
|
+
Metrics/MethodLength:
|
|
9
|
+
Max: 30
|
|
10
|
+
CountAsOne:
|
|
11
|
+
- "array"
|
|
12
|
+
- "heredoc"
|
|
13
|
+
- "method_call"
|
|
14
|
+
|
|
15
|
+
Metrics/ModuleLength:
|
|
16
|
+
Max: 150
|
|
17
|
+
CountAsOne:
|
|
18
|
+
- "array"
|
|
19
|
+
- "heredoc"
|
|
20
|
+
- "method_call"
|
data/cops/naming.yml
ADDED
data/cops/rspec.yml
ADDED
data/cops/style.yml
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Style/FrozenStringLiteralComment:
|
|
2
|
+
Enabled: false
|
|
3
|
+
|
|
4
|
+
# enforces to use 'name'.to_sym over 'name'.intern and so on
|
|
5
|
+
Style/StringMethods:
|
|
6
|
+
Enabled: true
|
|
7
|
+
|
|
8
|
+
# @NOTE: changed from "compact" to "expanded"
|
|
9
|
+
Style/EmptyMethod:
|
|
10
|
+
EnforcedStyle: expanded
|
|
11
|
+
SupportedStyles:
|
|
12
|
+
- compact
|
|
13
|
+
- expanded
|
|
14
|
+
|
|
15
|
+
# @NOTE: allowed AllowMultipleReturnValues
|
|
16
|
+
Style/RedundantReturn:
|
|
17
|
+
Enabled: true
|
|
18
|
+
AllowMultipleReturnValues: true
|
|
19
|
+
|
|
20
|
+
# @NOTE: allowed AllowAsExpressionSeparator
|
|
21
|
+
Style/Semicolon:
|
|
22
|
+
Enabled: true
|
|
23
|
+
AllowAsExpressionSeparator: true
|
|
24
|
+
|
|
25
|
+
Style/CollectionMethods:
|
|
26
|
+
Enabled: true
|
|
27
|
+
|
|
28
|
+
Style/Documentation:
|
|
29
|
+
Enabled: false
|
|
30
|
+
|
|
31
|
+
# @NOTE: changed EnforcedStyleForMultiline from "no_comma" to "comma"
|
|
32
|
+
Style/TrailingCommaInArguments:
|
|
33
|
+
EnforcedStyleForMultiline: comma
|
|
34
|
+
|
|
35
|
+
# @NOTE: changed EnforcedStyleForMultiline from "no_comma" to "comma"
|
|
36
|
+
Style/TrailingCommaInArrayLiteral:
|
|
37
|
+
EnforcedStyleForMultiline: comma
|
|
38
|
+
|
|
39
|
+
# @NOTE: changed EnforcedStyleForMultiline from "no_comma" to "comma"
|
|
40
|
+
Style/TrailingCommaInHashLiteral:
|
|
41
|
+
EnforcedStyleForMultiline: comma
|
|
42
|
+
|
|
43
|
+
#
|
|
44
|
+
# @NOTE: the following cops are "pending" by default
|
|
45
|
+
#
|
|
46
|
+
Style/ArgumentsForwarding:
|
|
47
|
+
Enabled: false
|
|
48
|
+
|
|
49
|
+
Style/CollectionCompact:
|
|
50
|
+
Enabled: true
|
|
51
|
+
|
|
52
|
+
Style/DocumentDynamicEvalDefinition:
|
|
53
|
+
Enabled: true
|
|
54
|
+
|
|
55
|
+
Style/NegatedIfElseCondition:
|
|
56
|
+
Enabled: true
|
|
57
|
+
|
|
58
|
+
Style/NilLambda:
|
|
59
|
+
Enabled: true
|
|
60
|
+
|
|
61
|
+
Style/RedundantArgument:
|
|
62
|
+
Enabled: true
|
|
63
|
+
|
|
64
|
+
Style/SwapValues:
|
|
65
|
+
Enabled: true
|
|
66
|
+
|
|
67
|
+
# "private_class_method" must always be inlined, no need for a cop
|
|
68
|
+
Style/AccessModifierDeclarations:
|
|
69
|
+
EnforcedStyle: inline
|
|
70
|
+
|
|
71
|
+
Style/StringLiterals:
|
|
72
|
+
Enabled: true
|
|
73
|
+
EnforcedStyle: double_quotes
|
|
74
|
+
|
|
75
|
+
Style/StringLiteralsInInterpolation:
|
|
76
|
+
Enabled: true
|
|
77
|
+
EnforcedStyle: double_quotes
|
data/cops/types.yml
ADDED
data/kirei.gemspec
CHANGED
|
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
# do not include RBIs for gems, because users might use different versions
|
|
35
35
|
"sorbet/rbi/dsl/**/*.rbi",
|
|
36
36
|
"sorbet/rbi/shims/**/*.rbi",
|
|
37
|
+
"cops/**/*",
|
|
37
38
|
"LICENSE",
|
|
38
39
|
"README.md",
|
|
39
40
|
]
|
|
@@ -46,7 +47,6 @@ Gem::Specification.new do |spec|
|
|
|
46
47
|
spec.add_dependency "logger", "~> 1.5" # for Ruby 3.5+
|
|
47
48
|
spec.add_dependency "oj", "~> 3.0"
|
|
48
49
|
spec.add_dependency "sorbet-runtime", "~> 0.5"
|
|
49
|
-
spec.add_dependency "statsd-instrument", "~> 3.0"
|
|
50
50
|
spec.add_dependency "tzinfo-data", "~> 1.0" # for containerized environments, e.g. on AWS ECS
|
|
51
51
|
spec.add_dependency "zeitwerk", "~> 2.5"
|
|
52
52
|
|
data/lib/kirei/config.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Kirei
|
|
|
21
21
|
prop :log_level, Kirei::Logging::Level, default: Kirei::Logging::Level::INFO
|
|
22
22
|
|
|
23
23
|
prop :metric_default_tags, T::Hash[String, T.untyped], default: {}
|
|
24
|
+
prop :metrics_backend, Kirei::Metrics::Backend, factory: -> { Kirei::Metrics::LoggingBackend.new }
|
|
24
25
|
|
|
25
26
|
# dup to allow the user to extend the existing list of sensitive keys
|
|
26
27
|
prop :sensitive_keys, T::Array[Regexp], factory: -> { SENSITIVE_KEYS.dup }
|
|
@@ -18,24 +18,6 @@ module Kirei
|
|
|
18
18
|
instance_variable_get(var) == other.instance_variable_get(var)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
|
-
|
|
22
|
-
sig do
|
|
23
|
-
params(
|
|
24
|
-
other: T.untyped,
|
|
25
|
-
array_mode: Kirei::Services::ArrayComparison::Mode,
|
|
26
|
-
).returns(T::Boolean)
|
|
27
|
-
end
|
|
28
|
-
def equal_with_array_mode?(other, array_mode: Kirei::Services::ArrayComparison::Mode::STRICT)
|
|
29
|
-
return false unless instance_of?(other.class)
|
|
30
|
-
|
|
31
|
-
instance_variables.all? do |var|
|
|
32
|
-
one = instance_variable_get(var)
|
|
33
|
-
two = other.instance_variable_get(var)
|
|
34
|
-
next one == two unless one.is_a?(Array)
|
|
35
|
-
|
|
36
|
-
Kirei::Services::ArrayComparison.call(one, two, mode: array_mode)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
21
|
end
|
|
40
22
|
end
|
|
41
23
|
end
|
data/lib/kirei/helpers.rb
CHANGED
|
@@ -10,19 +10,13 @@ module Kirei
|
|
|
10
10
|
sig { params(string: String).returns(String) }
|
|
11
11
|
def underscore(string)
|
|
12
12
|
string.gsub!(/([A-Z])(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) do
|
|
13
|
-
T.must(
|
|
13
|
+
T.must(::Regexp.last_match(1) || ::Regexp.last_match(2)) << "_"
|
|
14
14
|
end
|
|
15
15
|
string.tr!("-", "_")
|
|
16
16
|
string.downcase!
|
|
17
17
|
string
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# Simplified version from Rails' ActiveSupport
|
|
21
|
-
sig { params(string: T.any(String, Symbol)).returns(T::Boolean) }
|
|
22
|
-
def blank?(string)
|
|
23
|
-
string.nil? || string.to_s.empty?
|
|
24
|
-
end
|
|
25
|
-
|
|
26
20
|
sig { params(object: T.untyped).returns(T.untyped) }
|
|
27
21
|
def deep_stringify_keys(object)
|
|
28
22
|
deep_transform_keys(object) { _1.to_s rescue _1 } # rubocop:disable Style/RescueModifier
|
|
@@ -74,7 +68,7 @@ module Kirei
|
|
|
74
68
|
when Hash
|
|
75
69
|
# using `each_key` results in a `RuntimeError: can't add a new key into hash during iteration`
|
|
76
70
|
# which is, because the receiver here does not necessarily have a `Hash` type
|
|
77
|
-
object.keys.each do |key|
|
|
71
|
+
object.keys.each do |key|
|
|
78
72
|
value = object.delete(key)
|
|
79
73
|
object[yield(key)] = deep_transform_keys!(value, &block)
|
|
80
74
|
end
|
data/lib/kirei/logging/logger.rb
CHANGED
|
@@ -156,7 +156,7 @@ module Kirei
|
|
|
156
156
|
hash = T.cast(hash, T::Hash[String, T.untyped])
|
|
157
157
|
|
|
158
158
|
hash.each do |key, value|
|
|
159
|
-
new_prefix =
|
|
159
|
+
new_prefix = prefix.empty? ? key : "#{prefix}.#{key}"
|
|
160
160
|
|
|
161
161
|
case value
|
|
162
162
|
when Hash
|
data/lib/kirei/logging/metric.rb
CHANGED
|
@@ -14,13 +14,11 @@ module Kirei
|
|
|
14
14
|
).void
|
|
15
15
|
end
|
|
16
16
|
def self.call(metric_name, value = 1, tags: {})
|
|
17
|
-
return if ENV["NO_METRICS"] == "true"
|
|
18
|
-
|
|
19
17
|
inject_defaults(tags)
|
|
20
18
|
|
|
21
19
|
# Do not `compact_blank` tags, since one might want to track empty strings/"false"/NULLs.
|
|
22
20
|
# NOT having any tag doesn't tell the user if the tag was empty or not set at all.
|
|
23
|
-
|
|
21
|
+
App.config.metrics_backend.increment(metric_name, value, tags: tags)
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
sig { params(tags: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Kirei
|
|
5
|
+
module Metrics
|
|
6
|
+
class Backend
|
|
7
|
+
extend T::Sig
|
|
8
|
+
extend T::Helpers
|
|
9
|
+
|
|
10
|
+
abstract!
|
|
11
|
+
|
|
12
|
+
sig do
|
|
13
|
+
abstract.params(
|
|
14
|
+
name: String,
|
|
15
|
+
value: T.any(Integer, Float),
|
|
16
|
+
tags: T::Hash[String, T.untyped],
|
|
17
|
+
).void
|
|
18
|
+
end
|
|
19
|
+
def increment(name, value = 1, tags: {})
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
abstract.params(
|
|
24
|
+
name: String,
|
|
25
|
+
duration_ms: T.any(Integer, Float),
|
|
26
|
+
tags: T::Hash[String, T.untyped],
|
|
27
|
+
).void
|
|
28
|
+
end
|
|
29
|
+
def measure(name, duration_ms, tags: {})
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig do
|
|
33
|
+
abstract.params(
|
|
34
|
+
name: String,
|
|
35
|
+
value: T.any(Integer, Float),
|
|
36
|
+
tags: T::Hash[String, T.untyped],
|
|
37
|
+
).void
|
|
38
|
+
end
|
|
39
|
+
def gauge(name, value, tags: {})
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Kirei
|
|
5
|
+
module Metrics
|
|
6
|
+
class LoggingBackend < Backend
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig do
|
|
10
|
+
override.params(
|
|
11
|
+
name: String,
|
|
12
|
+
value: T.any(Integer, Float),
|
|
13
|
+
tags: T::Hash[String, T.untyped],
|
|
14
|
+
).void
|
|
15
|
+
end
|
|
16
|
+
def increment(name, value = 1, tags: {})
|
|
17
|
+
puts("[Metric] increment #{name} #{value} #{tags}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sig do
|
|
21
|
+
override.params(
|
|
22
|
+
name: String,
|
|
23
|
+
duration_ms: T.any(Integer, Float),
|
|
24
|
+
tags: T::Hash[String, T.untyped],
|
|
25
|
+
).void
|
|
26
|
+
end
|
|
27
|
+
def measure(name, duration_ms, tags: {})
|
|
28
|
+
puts("[Metric] measure #{name} #{duration_ms}ms #{tags}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig do
|
|
32
|
+
override.params(
|
|
33
|
+
name: String,
|
|
34
|
+
value: T.any(Integer, Float),
|
|
35
|
+
tags: T::Hash[String, T.untyped],
|
|
36
|
+
).void
|
|
37
|
+
end
|
|
38
|
+
def gauge(name, value, tags: {})
|
|
39
|
+
puts("[Metric] gauge #{name} #{value} #{tags}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Kirei
|
|
5
|
+
module Metrics
|
|
6
|
+
class NullBackend < Backend
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig do
|
|
10
|
+
override.params(
|
|
11
|
+
name: String,
|
|
12
|
+
value: T.any(Integer, Float),
|
|
13
|
+
tags: T::Hash[String, T.untyped],
|
|
14
|
+
).void
|
|
15
|
+
end
|
|
16
|
+
def increment(name, value = 1, tags: {})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig do
|
|
20
|
+
override.params(
|
|
21
|
+
name: String,
|
|
22
|
+
duration_ms: T.any(Integer, Float),
|
|
23
|
+
tags: T::Hash[String, T.untyped],
|
|
24
|
+
).void
|
|
25
|
+
end
|
|
26
|
+
def measure(name, duration_ms, tags: {})
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig do
|
|
30
|
+
override.params(
|
|
31
|
+
name: String,
|
|
32
|
+
value: T.any(Integer, Float),
|
|
33
|
+
tags: T::Hash[String, T.untyped],
|
|
34
|
+
).void
|
|
35
|
+
end
|
|
36
|
+
def gauge(name, value, tags: {})
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Kirei
|
|
5
|
+
module Metrics
|
|
6
|
+
class StatsdBackend < Backend
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { void }
|
|
10
|
+
def initialize
|
|
11
|
+
super
|
|
12
|
+
return if defined?(::StatsD)
|
|
13
|
+
|
|
14
|
+
raise "statsd-instrument is not loaded. Add `gem 'statsd-instrument'` to your Gemfile to use StatsdBackend."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig do
|
|
18
|
+
override.params(
|
|
19
|
+
name: String,
|
|
20
|
+
value: T.any(Integer, Float),
|
|
21
|
+
tags: T::Hash[String, T.untyped],
|
|
22
|
+
).void
|
|
23
|
+
end
|
|
24
|
+
def increment(name, value = 1, tags: {})
|
|
25
|
+
::StatsD.increment(name, value, tags: tags)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig do
|
|
29
|
+
override.params(
|
|
30
|
+
name: String,
|
|
31
|
+
duration_ms: T.any(Integer, Float),
|
|
32
|
+
tags: T::Hash[String, T.untyped],
|
|
33
|
+
).void
|
|
34
|
+
end
|
|
35
|
+
def measure(name, duration_ms, tags: {})
|
|
36
|
+
::StatsD.measure(name, duration_ms, tags: tags)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig do
|
|
40
|
+
override.params(
|
|
41
|
+
name: String,
|
|
42
|
+
value: T.any(Integer, Float),
|
|
43
|
+
tags: T::Hash[String, T.untyped],
|
|
44
|
+
).void
|
|
45
|
+
end
|
|
46
|
+
def gauge(name, value, tags: {})
|
|
47
|
+
::StatsD.gauge(name, value, tags: tags)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -85,13 +85,15 @@ module Kirei
|
|
|
85
85
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
86
86
|
|
|
87
87
|
sig { override.params(attributes: T::Hash[T.any(Symbol, String), T.untyped]).void }
|
|
88
|
-
def wrap_jsonb_non_primivitives!(attributes)
|
|
88
|
+
def wrap_jsonb_non_primivitives!(attributes) # rubocop:disable Metrics/AbcSize
|
|
89
89
|
# setting `@raw_db_connection.wrap_json_primitives = true`
|
|
90
90
|
# only works on JSON primitives, but not on blank hashes/arrays
|
|
91
91
|
return unless App.config.db_extensions.include?(:pg_json)
|
|
92
92
|
|
|
93
|
+
pgvector_enabled = App.config.db_extensions.include?(:pgvector)
|
|
94
|
+
|
|
93
95
|
attributes.each_pair do |key, value|
|
|
94
|
-
if vector_column?(key.to_s)
|
|
96
|
+
if pgvector_enabled && vector_column?(key.to_s)
|
|
95
97
|
attributes[key] = cast_to_vector(value)
|
|
96
98
|
elsif value.is_a?(Hash) || value.is_a?(Array)
|
|
97
99
|
attributes[key] = T.unsafe(Sequel).pg_jsonb_wrap(value)
|
|
@@ -104,6 +106,10 @@ module Kirei
|
|
|
104
106
|
# also add `:pgvector` to the `App.config.db_extensions` array
|
|
105
107
|
# and enable the vector extension on the database.
|
|
106
108
|
#
|
|
109
|
+
# Note: Sequel caches `db.schema` results internally (Database#cache_schema is true by default),
|
|
110
|
+
# so this only hits the database once per table per app lifecycle.
|
|
111
|
+
# @see https://github.com/jeremyevans/sequel/blob/master/lib/sequel/extensions/schema_caching.rb
|
|
112
|
+
#
|
|
107
113
|
sig { params(column_name: String).returns(T::Boolean) }
|
|
108
114
|
def vector_column?(column_name)
|
|
109
115
|
_col_name, col_info = T.let(
|
data/lib/kirei/model.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Kirei
|
|
|
25
25
|
# Delete keeps the original object intact. Returns true if the record was deleted.
|
|
26
26
|
# Calling delete multiple times will return false after the first (successful) call.
|
|
27
27
|
sig { returns(T::Boolean) }
|
|
28
|
-
def delete
|
|
28
|
+
def delete # rubocop:disable Naming/PredicateMethod
|
|
29
29
|
count = self.class.query.where({ id: id }).delete
|
|
30
30
|
count == 1
|
|
31
31
|
end
|
data/lib/kirei/routing/base.rb
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# rubocop:disable Metrics
|
|
5
|
-
|
|
4
|
+
# rubocop:disable Metrics
|
|
6
5
|
module Kirei
|
|
7
6
|
module Routing
|
|
8
7
|
class Base
|
|
@@ -36,8 +35,10 @@ module Kirei
|
|
|
36
35
|
#
|
|
37
36
|
|
|
38
37
|
lookup_verb = http_verb == Verb::HEAD ? Verb::GET : http_verb
|
|
39
|
-
|
|
40
|
-
return NOT_FOUND if
|
|
38
|
+
result = router.resolve(lookup_verb, req_path)
|
|
39
|
+
return NOT_FOUND if result.nil?
|
|
40
|
+
|
|
41
|
+
route, path_params = result
|
|
41
42
|
|
|
42
43
|
router.current_env = env # expose the env to the controller
|
|
43
44
|
|
|
@@ -67,6 +68,8 @@ module Kirei
|
|
|
67
68
|
T.absurd(http_verb)
|
|
68
69
|
end
|
|
69
70
|
|
|
71
|
+
params.merge!(path_params)
|
|
72
|
+
|
|
70
73
|
req_id = T.cast(env["HTTP_X_REQUEST_ID"], T.nilable(String))
|
|
71
74
|
req_id ||= "req_#{App.environment}_#{SecureRandom.uuid}"
|
|
72
75
|
Thread.current[:request_id] = req_id
|
|
@@ -125,7 +128,7 @@ module Kirei
|
|
|
125
128
|
stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
126
129
|
if start # early return for 404
|
|
127
130
|
latency_in_ms = stop - start
|
|
128
|
-
|
|
131
|
+
App.config.metrics_backend.measure("request", latency_in_ms, tags: statsd_timing_tags)
|
|
129
132
|
|
|
130
133
|
Kirei::Logging::Logger.call(
|
|
131
134
|
level: status >= 500 ? Kirei::Logging::Level::ERROR : Kirei::Logging::Level::INFO,
|
|
@@ -159,6 +162,56 @@ module Kirei
|
|
|
159
162
|
]
|
|
160
163
|
end
|
|
161
164
|
|
|
165
|
+
#
|
|
166
|
+
# Renders a JSON response. Accepts:
|
|
167
|
+
# - String: treated as pre-serialized JSON (pass-through)
|
|
168
|
+
# - Hash / Array: serialized via Oj.dump
|
|
169
|
+
# - Object responding to #serialize (e.g. T::Struct): calls #serialize,
|
|
170
|
+
# then Oj.dump if the result is not already a String
|
|
171
|
+
# - Anything else: raises ArgumentError
|
|
172
|
+
#
|
|
173
|
+
sig do
|
|
174
|
+
params(
|
|
175
|
+
data: T.untyped,
|
|
176
|
+
status: Integer,
|
|
177
|
+
headers: T::Hash[String, String],
|
|
178
|
+
).returns(RackResponseType)
|
|
179
|
+
end
|
|
180
|
+
def render_json(data, status: 200, headers: {})
|
|
181
|
+
body = case data
|
|
182
|
+
when String
|
|
183
|
+
data
|
|
184
|
+
when Hash, Array
|
|
185
|
+
Oj.dump(data, Kirei::OJ_OPTIONS)
|
|
186
|
+
else
|
|
187
|
+
unless data.respond_to?(:serialize)
|
|
188
|
+
raise ArgumentError,
|
|
189
|
+
"render_json expects a String, Hash, Array, or an object responding to #serialize, " \
|
|
190
|
+
"got #{data.class}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
result = data.serialize
|
|
194
|
+
result.is_a?(String) ? result : Oj.dump(result, Kirei::OJ_OPTIONS)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
render(body, status: status, headers: headers)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
#
|
|
201
|
+
# Renders a JSON:API-compliant error response.
|
|
202
|
+
# Wraps an array of JsonApiError structs into { "errors": [...] }.
|
|
203
|
+
#
|
|
204
|
+
sig do
|
|
205
|
+
params(
|
|
206
|
+
errors: T::Array[Errors::JsonApiError],
|
|
207
|
+
status: Integer,
|
|
208
|
+
headers: T::Hash[String, String],
|
|
209
|
+
).returns(RackResponseType)
|
|
210
|
+
end
|
|
211
|
+
def render_error(errors, status: 422, headers: {})
|
|
212
|
+
render_json({ "errors" => errors.map(&:serialize) }, status: status, headers: headers)
|
|
213
|
+
end
|
|
214
|
+
|
|
162
215
|
sig { returns(T::Hash[String, String]) }
|
|
163
216
|
def default_headers
|
|
164
217
|
{
|
|
@@ -223,5 +276,4 @@ module Kirei
|
|
|
223
276
|
end
|
|
224
277
|
end
|
|
225
278
|
end
|
|
226
|
-
|
|
227
|
-
# rubocop:enable Metrics/all
|
|
279
|
+
# rubocop:enable Metrics
|
data/lib/kirei/routing/route.rb
CHANGED
|
@@ -4,10 +4,22 @@
|
|
|
4
4
|
module Kirei
|
|
5
5
|
module Routing
|
|
6
6
|
class Route < T::Struct
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
7
9
|
const :verb, Verb
|
|
8
10
|
const :path, String
|
|
9
11
|
const :controller, T.class_of(Controller)
|
|
10
12
|
const :action, String
|
|
13
|
+
|
|
14
|
+
sig { returns(T::Array[String]) }
|
|
15
|
+
def segments
|
|
16
|
+
@segments ||= T.let(path.split("/"), T.nilable(T::Array[String]))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { returns(T::Boolean) }
|
|
20
|
+
def dynamic?
|
|
21
|
+
segments.any? { |s| s.start_with?(":") }
|
|
22
|
+
end
|
|
11
23
|
end
|
|
12
24
|
end
|
|
13
25
|
end
|
data/lib/kirei/routing/router.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
# rubocop:disable Metrics
|
|
5
|
+
|
|
4
6
|
require("singleton")
|
|
5
7
|
|
|
6
8
|
module Kirei
|
|
@@ -25,17 +27,26 @@ module Kirei
|
|
|
25
27
|
T::Hash[String, Route]
|
|
26
28
|
end
|
|
27
29
|
|
|
30
|
+
ResolveResult = T.type_alias do
|
|
31
|
+
T.nilable([Route, T::Hash[String, String]])
|
|
32
|
+
end
|
|
33
|
+
|
|
28
34
|
sig { returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
29
35
|
attr_accessor :current_env
|
|
30
36
|
|
|
31
37
|
sig { void }
|
|
32
38
|
def initialize
|
|
33
39
|
@routes = T.let({}, RoutesHash)
|
|
40
|
+
@dynamic_routes = T.let([], T::Array[Route])
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
sig { returns(RoutesHash) }
|
|
37
44
|
attr_reader :routes
|
|
38
45
|
|
|
46
|
+
sig { returns(T::Array[Route]) }
|
|
47
|
+
attr_reader :dynamic_routes
|
|
48
|
+
|
|
49
|
+
# Looks up a static route by exact verb + path match. O(1).
|
|
39
50
|
sig do
|
|
40
51
|
params(
|
|
41
52
|
verb: Verb,
|
|
@@ -47,13 +58,72 @@ module Kirei
|
|
|
47
58
|
routes[key]
|
|
48
59
|
end
|
|
49
60
|
|
|
61
|
+
# Resolves a request to a route and extracted path parameters.
|
|
62
|
+
# Tries static O(1) lookup first, then falls back to dynamic segment matching.
|
|
63
|
+
sig do
|
|
64
|
+
params(
|
|
65
|
+
verb: Verb,
|
|
66
|
+
path: String,
|
|
67
|
+
).returns(ResolveResult)
|
|
68
|
+
end
|
|
69
|
+
def resolve(verb, path)
|
|
70
|
+
static_route = get(verb, path)
|
|
71
|
+
return [static_route, {}] unless static_route.nil?
|
|
72
|
+
|
|
73
|
+
match_dynamic(verb, path)
|
|
74
|
+
end
|
|
75
|
+
|
|
50
76
|
sig { params(routes: T::Array[Route]).void }
|
|
51
77
|
def self.add_routes(routes)
|
|
52
78
|
routes.each do |route|
|
|
53
|
-
|
|
54
|
-
|
|
79
|
+
if route.dynamic?
|
|
80
|
+
instance.dynamic_routes << route
|
|
81
|
+
else
|
|
82
|
+
key = "#{route.verb.serialize} #{route.path}"
|
|
83
|
+
instance.routes[key] = route
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Matches a request path against registered dynamic routes.
|
|
89
|
+
# Returns [Route, extracted_params] or nil.
|
|
90
|
+
sig do
|
|
91
|
+
params(
|
|
92
|
+
verb: Verb,
|
|
93
|
+
path: String,
|
|
94
|
+
).returns(ResolveResult)
|
|
95
|
+
end
|
|
96
|
+
private def match_dynamic(verb, path)
|
|
97
|
+
request_segments = path.split("/")
|
|
98
|
+
|
|
99
|
+
dynamic_routes.each do |route|
|
|
100
|
+
next unless route.verb == verb
|
|
101
|
+
|
|
102
|
+
route_segments = route.segments
|
|
103
|
+
next unless route_segments.length == request_segments.length
|
|
104
|
+
|
|
105
|
+
path_params = T.let({}, T::Hash[String, String])
|
|
106
|
+
matched = T.let(true, T::Boolean)
|
|
107
|
+
|
|
108
|
+
route_segments.each_with_index do |route_seg, idx|
|
|
109
|
+
req_seg = T.must(request_segments[idx])
|
|
110
|
+
|
|
111
|
+
if route_seg.start_with?(":")
|
|
112
|
+
param_name = T.must(route_seg[1..])
|
|
113
|
+
path_params[param_name] = req_seg
|
|
114
|
+
elsif route_seg != req_seg
|
|
115
|
+
matched = false
|
|
116
|
+
break
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
return [route, path_params] if matched
|
|
55
121
|
end
|
|
122
|
+
|
|
123
|
+
nil
|
|
56
124
|
end
|
|
57
125
|
end
|
|
58
126
|
end
|
|
59
127
|
end
|
|
128
|
+
|
|
129
|
+
# rubocop:enable Metrics
|
|
@@ -27,7 +27,7 @@ module Kirei
|
|
|
27
27
|
result = service_result(service)
|
|
28
28
|
|
|
29
29
|
metric_tags = Logging::Metric.inject_defaults({ "service.result" => result })
|
|
30
|
-
|
|
30
|
+
App.config.metrics_backend.measure(class_name, latency_in_ms, tags: metric_tags)
|
|
31
31
|
|
|
32
32
|
logtags = {
|
|
33
33
|
"service.name" => class_name.to_s,
|
data/lib/kirei/version.rb
CHANGED
data/lib/kirei.rb
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
3
|
+
# Minimal shim for `statsd-instrument`.
|
|
4
|
+
# The gem is an optional dependency — this shim lets Sorbet
|
|
5
|
+
# type-check `StatsdBackend` without requiring the gem at analysis time.
|
|
6
|
+
module StatsD
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { params(name: String, value: T.any(Integer, Float), tags: T.untyped).void }
|
|
10
|
+
def self.increment(name, value = 1, tags: {}); end
|
|
11
|
+
|
|
12
|
+
sig { params(name: String, value: T.any(Integer, Float), tags: T.untyped).void }
|
|
13
|
+
def self.measure(name, value, tags: {}); end
|
|
14
|
+
|
|
15
|
+
sig { params(name: String, value: T.any(Integer, Float), tags: T.untyped).void }
|
|
16
|
+
def self.gauge(name, value, tags: {}); end
|
|
17
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kirei
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ludwig Reinmiedl
|
|
@@ -51,20 +51,6 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.5'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: statsd-instrument
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '3.0'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '3.0'
|
|
68
54
|
- !ruby/object:Gem::Dependency
|
|
69
55
|
name: tzinfo-data
|
|
70
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -165,6 +151,13 @@ files:
|
|
|
165
151
|
- ".irbrc"
|
|
166
152
|
- README.md
|
|
167
153
|
- bin/kirei
|
|
154
|
+
- cops/layout.yml
|
|
155
|
+
- cops/lint.yml
|
|
156
|
+
- cops/metrics.yml
|
|
157
|
+
- cops/naming.yml
|
|
158
|
+
- cops/rspec.yml
|
|
159
|
+
- cops/style.yml
|
|
160
|
+
- cops/types.yml
|
|
168
161
|
- kirei.gemspec
|
|
169
162
|
- lib/cli.rb
|
|
170
163
|
- lib/cli/commands/new_app/base_directories.rb
|
|
@@ -189,6 +182,10 @@ files:
|
|
|
189
182
|
- lib/kirei/logging/level.rb
|
|
190
183
|
- lib/kirei/logging/logger.rb
|
|
191
184
|
- lib/kirei/logging/metric.rb
|
|
185
|
+
- lib/kirei/metrics/backend.rb
|
|
186
|
+
- lib/kirei/metrics/logging_backend.rb
|
|
187
|
+
- lib/kirei/metrics/null_backend.rb
|
|
188
|
+
- lib/kirei/metrics/statsd_backend.rb
|
|
192
189
|
- lib/kirei/model.rb
|
|
193
190
|
- lib/kirei/model/base_class_interface.rb
|
|
194
191
|
- lib/kirei/model/class_methods.rb
|
|
@@ -201,7 +198,6 @@ files:
|
|
|
201
198
|
- lib/kirei/routing/route.rb
|
|
202
199
|
- lib/kirei/routing/router.rb
|
|
203
200
|
- lib/kirei/routing/verb.rb
|
|
204
|
-
- lib/kirei/services/array_comparison.rb
|
|
205
201
|
- lib/kirei/services/result.rb
|
|
206
202
|
- lib/kirei/services/runner.rb
|
|
207
203
|
- lib/kirei/version.rb
|
|
@@ -209,6 +205,7 @@ files:
|
|
|
209
205
|
- sorbet/rbi/shims/base_model.rbi
|
|
210
206
|
- sorbet/rbi/shims/domain.rbi
|
|
211
207
|
- sorbet/rbi/shims/ruby.rbi
|
|
208
|
+
- sorbet/rbi/shims/statsd.rbi
|
|
212
209
|
homepage: https://github.com/swiknaba/kirei
|
|
213
210
|
licenses:
|
|
214
211
|
- MIT
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# typed: strict
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
module Kirei
|
|
5
|
-
module Services
|
|
6
|
-
class ArrayComparison
|
|
7
|
-
extend T::Sig
|
|
8
|
-
|
|
9
|
-
class Mode < T::Enum
|
|
10
|
-
enums do
|
|
11
|
-
STRICT = new("strict")
|
|
12
|
-
IGNORE_ORDER = new("ignore_order")
|
|
13
|
-
IGNORE_ORDER_AND_DUPLICATES = new("ignore_order_and_duplicates")
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
sig do
|
|
18
|
-
params(
|
|
19
|
-
array_one: T::Array[T.untyped],
|
|
20
|
-
array_two: T::Array[T.untyped],
|
|
21
|
-
mode: Mode,
|
|
22
|
-
).returns(T::Boolean)
|
|
23
|
-
end
|
|
24
|
-
def self.call(array_one, array_two, mode: Mode::STRICT)
|
|
25
|
-
case mode
|
|
26
|
-
when Mode::STRICT then array_one == array_two
|
|
27
|
-
when Mode::IGNORE_ORDER then array_one.sort == array_two.sort
|
|
28
|
-
when Mode::IGNORE_ORDER_AND_DUPLICATES then array_one.to_set == array_two.to_set
|
|
29
|
-
else
|
|
30
|
-
T.absurd(mode)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|