kirei 0.0.3 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b67435621f32def7ede1b291b5d0fe529125bda4dd34149afa5b7db6d2193ddd
4
- data.tar.gz: fb6c0e91a22bdb8cf726f638c5a250c5245c9a2de4e662f853d2b436a865d2fe
3
+ metadata.gz: cea2982351607eeafadd7e8ee3bb3ce13ba2c7e1e37eed22fa2f5a124b625022
4
+ data.tar.gz: 84a47be77945fa7a2faf5950441290fde74293388467729f2eb52c1c6041d29b
5
5
  SHA512:
6
- metadata.gz: 5529b60454e2389fb5df0ff5fad1cdc5f3b1c8385396aa3f65d977db9bb6ceccbd9e93de70de7dba4f719681f8332cc30a2dfd087117589c274b7ad5f99926ef
7
- data.tar.gz: 61f28174f219f9968e59183d473a4296d0fe659e3104b4ab2debe8e7cef27de077f653279a9897f7fc71af541597b8b90c3cf8ef66c0cbbadd40984f1520d1a4
6
+ metadata.gz: 3b420ea6cbfce60bf741bfd85cbfeb310dfd71031519a6d5fcc330bff788d24a0df1ef7268abbdbe49a2f373f4ff5df413dc6cf1b83589f96a6f8867164ffcef
7
+ data.tar.gz: 061a15d94de3e73dcd8d41db48601bfa5390d59aa1438c322d590ec3c33db9da4dd38d1f9258bf37a13c248ded433883a6da9ff52d41ee5873ca13b0cda414ca
data/kirei.gemspec CHANGED
@@ -50,7 +50,6 @@ Gem::Specification.new do |spec|
50
50
  spec.add_dependency "tzinfo-data", "~> 1.0" # for containerized environments, e.g. on AWS ECS
51
51
 
52
52
  # Web server & routing
53
- spec.add_dependency "puma", "~> 6.0"
54
53
  spec.add_dependency "sinatra", "~> 3.0"
55
54
  spec.add_dependency "sinatra-contrib", "~> 3.0"
56
55
 
data/lib/boot.rb CHANGED
@@ -15,16 +15,10 @@ require "bundler/setup"
15
15
  require "logger"
16
16
  require "sorbet-runtime"
17
17
  require "oj"
18
- require "puma"
19
18
  require "sinatra"
20
19
  require "sinatra/namespace" # from sinatra-contrib
21
20
  require "pg"
22
21
  require "sequel" # "sequel_pg" is auto-required by "sequel"
23
22
 
24
- Oj.default_options = {
25
- mode: :compat, # required to dump hashes with symbol-keys
26
- symbol_keys: false, # T::Struct.new works only with string-keys
27
- }
28
-
29
23
  # Third: load all application code
30
24
  Dir[File.join(__dir__, "kirei/**/*.rb")].each { require(_1) }
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: true
2
2
 
3
3
  require "fileutils"
4
4
 
@@ -9,6 +9,7 @@ module Cli
9
9
  case args[0]
10
10
  when "new"
11
11
  app_name = args[1] || "MyApp"
12
+ # @TODO(lud, 31.12.2023): classify is from ActiveSupport -> remove this?
12
13
  app_name = app_name.gsub(/[-\s]/, "_").classify
13
14
  NewApp::Execute.call(app_name: app_name)
14
15
  else
@@ -55,7 +55,12 @@ module Kirei
55
55
  @raw_db_connection = Sequel.connect(AppBase.config.db_url || default_db_url)
56
56
 
57
57
  config.db_extensions.each do |ext|
58
- @raw_db_connection.extension(ext)
58
+ T.cast(@raw_db_connection, Sequel::Database).extension(ext)
59
+ end
60
+
61
+ if config.db_extensions.include?(:pg_json)
62
+ # https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb#L8
63
+ @raw_db_connection.wrap_json_primitives = true
59
64
  end
60
65
 
61
66
  @raw_db_connection
@@ -16,10 +16,37 @@ module Kirei
16
16
  ).returns(T.self_type)
17
17
  end
18
18
  def update(hash)
19
+ hash[:updated_at] = Time.now.utc if respond_to?(:updated_at) && hash[:updated_at].nil?
20
+ self.class.wrap_jsonb_non_primivitives!(hash)
19
21
  self.class.db.where({ id: id }).update(hash)
20
22
  self.class.find_by({ id: id })
21
23
  end
22
24
 
25
+ # Delete keeps the original object intact. Returns true if the record was deleted.
26
+ # Calling delete multiple times will return false after the first (successful) call.
27
+ sig { returns(T::Boolean) }
28
+ def delete
29
+ count = self.class.db.where({ id: id }).delete
30
+ count == 1
31
+ end
32
+
33
+ # warning: this is not concurrency-safe
34
+ # save keeps the original object intact, and returns a new object with the updated values.
35
+ sig { returns(T.self_type) }
36
+ def save
37
+ previous_record = self.class.find_by({ id: id })
38
+
39
+ hash = serialize
40
+ Helpers.deep_symbolize_keys!(hash)
41
+ hash = T.cast(hash, T::Hash[Symbol, T.untyped])
42
+
43
+ if previous_record.nil?
44
+ self.class.create(hash)
45
+ else
46
+ update(hash)
47
+ end
48
+ end
49
+
23
50
  module BaseClassInterface
24
51
  extend T::Sig
25
52
  extend T::Helpers
@@ -33,6 +60,14 @@ module Kirei
33
60
  def where(hash)
34
61
  end
35
62
 
63
+ sig { abstract.params(hash: T.untyped).returns(T.untyped) }
64
+ def create(hash)
65
+ end
66
+
67
+ sig { abstract.params(attributes: T.untyped).void }
68
+ def wrap_jsonb_non_primivitives!(attributes)
69
+ end
70
+
36
71
  sig { abstract.params(hash: T.untyped).returns(T.untyped) }
37
72
  def resolve(hash)
38
73
  end
@@ -88,6 +123,49 @@ module Kirei
88
123
  resolve(db.where(hash))
89
124
  end
90
125
 
126
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
127
+ # default values defined in the model are used, if omitted in the hash
128
+ sig do
129
+ override.params(
130
+ hash: T::Hash[Symbol, T.untyped],
131
+ ).returns(T.attached_class)
132
+ end
133
+ def create(hash)
134
+ # instantiate a new object to ensure we use default values defined in the model
135
+ without_id = !hash.key?(:id)
136
+ hash[:id] = "kirei-fake-id" if without_id
137
+ new_record = from_hash(Helpers.deep_stringify_keys(hash))
138
+ all_attributes = T.let(new_record.serialize, T::Hash[String, T.untyped])
139
+ all_attributes.delete("id") if without_id && all_attributes["id"] == "kirei-fake-id"
140
+
141
+ wrap_jsonb_non_primivitives!(all_attributes)
142
+
143
+ if new_record.respond_to?(:created_at) && all_attributes["created_at"].nil?
144
+ all_attributes["created_at"] = Time.now.utc
145
+ end
146
+ if new_record.respond_to?(:updated_at) && all_attributes["updated_at"].nil?
147
+ all_attributes["updated_at"] = Time.now.utc
148
+ end
149
+
150
+ pkey = T.let(db.insert(all_attributes), String)
151
+
152
+ T.must(find_by({ id: pkey }))
153
+ end
154
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
155
+
156
+ sig { override.params(attributes: T::Hash[T.any(Symbol, String), T.untyped]).void }
157
+ def wrap_jsonb_non_primivitives!(attributes)
158
+ # setting `@raw_db_connection.wrap_json_primitives = true`
159
+ # only works on JSON primitives, but not on blank hashes/arrays
160
+ return unless AppBase.config.db_extensions.include?(:pg_json)
161
+
162
+ attributes.each_pair do |key, value|
163
+ next unless value.is_a?(Hash) || value.is_a?(Array)
164
+
165
+ attributes[key] = T.unsafe(Sequel).pg_jsonb_wrap(value)
166
+ end
167
+ end
168
+
91
169
  sig do
92
170
  override.params(
93
171
  hash: T::Hash[Symbol, T.untyped],
@@ -125,7 +203,9 @@ module Kirei
125
203
  ).returns(T.nilable(T.attached_class))
126
204
  end
127
205
  def resolve_first(query, strict = nil)
128
- resolve(query.limit(1), strict).first
206
+ strict_loading = strict.nil? ? AppBase.config.db_strict_type_resolving : strict
207
+
208
+ resolve(query.limit(1), strict_loading).first
129
209
  end
130
210
  end
131
211
 
data/lib/kirei/config.rb CHANGED
@@ -17,16 +17,18 @@ module Kirei
17
17
 
18
18
  prop :logger, ::Logger, factory: -> { ::Logger.new($stdout) }
19
19
  prop :log_transformer, T.nilable(T.proc.params(msg: T::Hash[Symbol, T.untyped]).returns(T::Array[String]))
20
+ prop :log_default_metadata, T::Hash[Symbol, String], default: {}
21
+
20
22
  # dup to allow the user to extend the existing list of sensitive keys
21
23
  prop :sensitive_keys, T::Array[Regexp], factory: -> { SENSITIVE_KEYS.dup }
24
+
22
25
  prop :app_name, String, default: "kirei"
23
- prop :db_url, T.nilable(String)
24
26
 
25
27
  # must use "pg_json" to parse jsonb columns to hashes
26
28
  #
27
29
  # Source: https://github.com/jeremyevans/sequel/blob/5.75.0/lib/sequel/extensions/pg_json.rb
28
30
  prop :db_extensions, T::Array[Symbol], default: %i[pg_json pg_array]
29
-
31
+ prop :db_url, T.nilable(String)
30
32
  # Extra or unknown properties present in the Hash do not raise exceptions at runtime
31
33
  # unless the optional strict argument to from_hash is passed
32
34
  #
data/lib/kirei/helpers.rb CHANGED
@@ -23,6 +23,26 @@ module Kirei
23
23
  string.nil? || string.to_s.empty?
24
24
  end
25
25
 
26
+ sig { params(object: T.untyped).returns(T.untyped) }
27
+ def deep_stringify_keys(object)
28
+ deep_transform_keys(object) { _1.to_s rescue _1 } # rubocop:disable Style/RescueModifier
29
+ end
30
+
31
+ sig { params(object: T.untyped).returns(T.untyped) }
32
+ def deep_stringify_keys!(object)
33
+ deep_transform_keys!(object) { _1.to_s rescue _1 } # rubocop:disable Style/RescueModifier
34
+ end
35
+
36
+ sig { params(object: T.untyped).returns(T.untyped) }
37
+ def deep_symbolize_keys(object)
38
+ deep_transform_keys(object) { _1.to_sym rescue _1 } # rubocop:disable Style/RescueModifier
39
+ end
40
+
41
+ sig { params(object: T.untyped).returns(T.untyped) }
42
+ def deep_symbolize_keys!(object)
43
+ deep_transform_keys!(object) { _1.to_sym rescue _1 } # rubocop:disable Style/RescueModifier
44
+ end
45
+
26
46
  # Simplified version from Rails' ActiveSupport
27
47
  sig do
28
48
  params(
@@ -30,7 +50,7 @@ module Kirei
30
50
  block: Proc,
31
51
  ).returns(T.untyped) # could be anything due to recursive calls
32
52
  end
33
- def deep_transform_keys(object, &block)
53
+ private def deep_transform_keys(object, &block)
34
54
  case object
35
55
  when Hash
36
56
  object.each_with_object({}) do |(key, value), result|
@@ -49,7 +69,7 @@ module Kirei
49
69
  block: Proc,
50
70
  ).returns(T.untyped) # could be anything due to recursive calls
51
71
  end
52
- def deep_transform_keys!(object, &block)
72
+ private def deep_transform_keys!(object, &block)
53
73
  case object
54
74
  when Hash
55
75
  # using `each_key` results in a `RuntimeError: can't add a new key into hash during iteration`
data/lib/kirei/logger.rb CHANGED
@@ -17,12 +17,12 @@ module Kirei
17
17
  #
18
18
  # You can define a custom log transformer to transform the logline:
19
19
  #
20
- # Kirei.config.log_transformer = Proc.new { _1 }
20
+ # Kirei::AppBase.config.log_transformer = Proc.new { _1 }
21
21
  #
22
- # By default, "meta" is flattened, and sensitive values are masked using see `Kirei.config.sensitive_keys`.
22
+ # By default, "meta" is flattened, and sensitive values are masked using see `Kirei::AppBase.config.sensitive_keys`.
23
23
  # You can also build on top of the provided log transformer:
24
24
  #
25
- # Kirei.config.log_transformer = Proc.new do |meta|
25
+ # Kirei::AppBase.config.log_transformer = Proc.new do |meta|
26
26
  # flattened_meta = Kirei::Logger.flatten_hash_and_mask_sensitive_values(meta)
27
27
  # # Do something with the flattened meta
28
28
  # flattened_meta.map { _1.to_json }
@@ -83,7 +83,16 @@ module Kirei
83
83
  ).void
84
84
  end
85
85
  def call(level:, label:, meta: {})
86
+ Kirei::AppBase.config.log_default_metadata.each_pair do |key, value|
87
+ meta[key] ||= value
88
+ end
89
+
90
+ #
91
+ # key names follow OpenTelemetry Semantic Conventions
92
+ # Source: https://opentelemetry.io/docs/concepts/semantic-conventions/
93
+ #
86
94
  meta[:"service.instance.id"] ||= Thread.current[:request_id]
95
+ meta[:"service.name"] ||= Kirei::AppBase.config.app_name
87
96
 
88
97
  # The Ruby logger only accepts one string as the only argument
89
98
  @queue << { level: level, label: label, meta: meta }
@@ -107,7 +116,10 @@ module Kirei
107
116
  loglines = if log_transformer
108
117
  log_transformer.call(meta)
109
118
  else
110
- [Oj.dump(Kirei::Logger.flatten_hash_and_mask_sensitive_values(meta))]
119
+ [Oj.dump(
120
+ Kirei::Logger.flatten_hash_and_mask_sensitive_values(meta),
121
+ Kirei::OJ_OPTIONS,
122
+ )]
111
123
  end
112
124
 
113
125
  loglines.each { Kirei::Logger.logger.error(_1) }
@@ -131,19 +143,24 @@ module Kirei
131
143
 
132
144
  sig do
133
145
  params(
134
- hash: T::Hash[Symbol, T.untyped],
146
+ hash: T::Hash[T.any(Symbol, String), T.untyped],
135
147
  prefix: Symbol,
136
148
  ).returns(T::Hash[Symbol, T.untyped])
137
149
  end
138
150
  def self.flatten_hash_and_mask_sensitive_values(hash, prefix = :'')
139
151
  result = T.let({}, T::Hash[Symbol, T.untyped])
140
- Kirei::Helpers.deep_transform_keys!(hash) { _1.to_sym rescue _1 } # rubocop:disable Style/RescueModifier
152
+ Kirei::Helpers.deep_symbolize_keys!(hash)
153
+ hash = T.cast(hash, T::Hash[Symbol, T.untyped])
141
154
 
142
155
  hash.each do |key, value|
143
156
  new_prefix = Kirei::Helpers.blank?(prefix) ? key : :"#{prefix}.#{key}"
144
157
 
145
158
  case value
146
- when Hash then result.merge!(flatten_hash_and_mask_sensitive_values(value.transform_keys(&:to_sym), new_prefix))
159
+ when Hash
160
+ # Some libraries have a custom Hash class that inhert from Hash, but act differently, e.g. OmniAuth::AuthHash.
161
+ # This results in `transform_keys` being available but without any effect.
162
+ value = value.to_h if value.class != Hash
163
+ result.merge!(flatten_hash_and_mask_sensitive_values(value.transform_keys(&:to_sym), new_prefix))
147
164
  when Array
148
165
  value.each_with_index do |element, index|
149
166
  if element.is_a?(Hash) || element.is_a?(Array)
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.0.3"
5
+ VERSION = "0.1.0"
6
6
  end
data/lib/kirei.rb CHANGED
@@ -6,6 +6,17 @@ require "boot"
6
6
  module Kirei
7
7
  extend T::Sig
8
8
 
9
+ # we don't know what Oj does under the hood with the options hash, so don't freeze it
10
+ # rubocop:disable Style/MutableConstant
11
+ OJ_OPTIONS = T.let(
12
+ {
13
+ mode: :compat, # required to dump hashes with symbol-keys
14
+ symbol_keys: false, # T::Struct.new works only with string-keys
15
+ },
16
+ T::Hash[Symbol, T.untyped],
17
+ )
18
+ # rubocop:enable Style/MutableConstant
19
+
9
20
  GEM_ROOT = T.let(
10
21
  Gem::Specification.find_by_name("kirei").gem_dir,
11
22
  String,
@@ -3,6 +3,9 @@
3
3
  # rubocop:disable Style/EmptyMethod
4
4
  module Kirei
5
5
  module BaseModel
6
+ include Kernel # "self" is a class since we include the module in a class
7
+ include T::Props::Serializable
8
+
6
9
  sig { returns(T.any(String, Integer)) }
7
10
  def id; end
8
11
 
@@ -12,6 +15,10 @@ module Kirei
12
15
  sig { returns(String) }
13
16
  def name; end
14
17
  end
18
+
19
+ module BaseClassInterface
20
+ # include T::Props::Serializable::ClassMethods
21
+ end
15
22
  end
16
23
  end
17
24
  # rubocop:enable Style/EmptyMethod
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.0.3
4
+ version: 0.1.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: 2023-12-31 00:00:00.000000000 Z
11
+ date: 2024-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.0'
69
- - !ruby/object:Gem::Dependency
70
- name: puma
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '6.0'
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '6.0'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: sinatra
85
71
  requirement: !ruby/object:Gem::Requirement