kirei 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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