ree_lib 1.3.9 → 1.3.11

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile.lock +2 -2
  4. data/lib/ree_lib/Packages.schema.json +4 -0
  5. data/lib/ree_lib/packages/ree_audit/.rspec +2 -0
  6. data/lib/ree_lib/packages/ree_audit/package/ree_audit/beans/audit.rb +83 -0
  7. data/lib/ree_lib/packages/ree_audit/package/ree_audit/config.rb +62 -0
  8. data/lib/ree_lib/packages/ree_audit/package/ree_audit/context.rb +44 -0
  9. data/lib/ree_lib/packages/ree_audit/package/ree_audit/event.rb +78 -0
  10. data/lib/ree_lib/packages/ree_audit/package/ree_audit/functions/filter_params.rb +75 -0
  11. data/lib/ree_lib/packages/ree_audit/package/ree_audit/sinks/logger_sink.rb +15 -0
  12. data/lib/ree_lib/packages/ree_audit/package/ree_audit/sinks/null_sink.rb +11 -0
  13. data/lib/ree_lib/packages/ree_audit/package/ree_audit/sinks/sink.rb +10 -0
  14. data/lib/ree_lib/packages/ree_audit/package/ree_audit.rb +37 -0
  15. data/lib/ree_lib/packages/ree_audit/spec/ree_audit/beans/audit_spec.rb +161 -0
  16. data/lib/ree_lib/packages/ree_audit/spec/ree_audit/context_spec.rb +47 -0
  17. data/lib/ree_lib/packages/ree_audit/spec/ree_audit/functions/filter_params_spec.rb +58 -0
  18. data/lib/ree_lib/packages/ree_audit/spec/spec_helper.rb +13 -0
  19. data/lib/ree_lib/packages/ree_dao/package/ree_dao/dataset_extensions.rb +15 -13
  20. data/lib/ree_lib/packages/ree_dao/spec/ree_dao/db_load_performance_mode_spec.rb +190 -0
  21. data/lib/ree_lib/packages/ree_dto/package/ree_dto/dto/dto_class_methods.rb +14 -0
  22. data/lib/ree_lib/packages/ree_dto/spec/ree_dto/build_from_attrs_spec.rb +48 -0
  23. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/db_load_compiler.rb +145 -0
  24. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/mapper.rb +25 -0
  25. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/mapper_strategy.rb +1 -3
  26. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/abstract_type.rb +9 -0
  27. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/any.rb +4 -0
  28. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/bool.rb +4 -0
  29. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/date.rb +5 -0
  30. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/date_time.rb +4 -0
  31. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/float.rb +4 -0
  32. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/integer.rb +4 -0
  33. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/rational.rb +4 -0
  34. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/string.rb +4 -0
  35. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/time.rb +5 -0
  36. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/wrappers/abstract_wrapper.rb +5 -0
  37. data/lib/ree_lib/packages/ree_mapper/package/ree_mapper.rb +6 -0
  38. data/lib/ree_lib/packages/ree_mapper/spec/ree_mapper/benchmarks/db_load_benchmark_spec.rb +218 -0
  39. data/lib/ree_lib/packages/ree_mapper/spec/ree_mapper/db_load_performance_mode_spec.rb +339 -0
  40. data/lib/ree_lib/packages/ree_roda/package/ree_roda/plugins/ree_routes.rb +29 -1
  41. data/lib/ree_lib/packages/ree_roda/package/ree_roda.rb +1 -0
  42. data/lib/ree_lib/packages/ree_roda/spec/ree_roda/plugins/ree_routes_audit_spec.rb +281 -0
  43. data/lib/ree_lib/packages/ree_roda/spec/spec_helper.rb +3 -0
  44. data/lib/ree_lib/packages/ree_routes/package/ree_routes/route.rb +11 -1
  45. data/lib/ree_lib/packages/ree_routes/package/ree_routes/route_builder.rb +7 -0
  46. data/lib/ree_lib/packages/ree_routes/spec/ree_routes/dsl_spec.rb +43 -0
  47. data/lib/ree_lib/version.rb +1 -1
  48. metadata +21 -1
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ package_require('ree_audit/functions/filter_params')
4
+
5
+ RSpec.describe :filter_params do
6
+ link :filter_params, from: :ree_audit
7
+
8
+ it "leaves a plain payload untouched" do
9
+ expect(filter_params({page: 1, per_page: 25})).to eq({page: 1, per_page: 25})
10
+ end
11
+
12
+ it "filters a secret sitting at the top level" do
13
+ expect(filter_params({password: "hunter2", page: 1})).to eq(
14
+ {password: 'FILTERED', page: 1}
15
+ )
16
+ end
17
+
18
+ it "filters a secret nested three levels deep" do
19
+ result = filter_params({user: {profile: {user_password: "hunter2", name: "Ann"}}})
20
+
21
+ expect(result).to eq(
22
+ {user: {profile: {user_password: 'FILTERED', name: "Ann"}}}
23
+ )
24
+ end
25
+
26
+ it "filters inside an array of hashes" do
27
+ result = filter_params({items: [{auth_token: "t"}, {name: "ok"}]})
28
+
29
+ expect(result).to eq({items: [{auth_token: 'FILTERED'}, {name: "ok"}]})
30
+ end
31
+
32
+ it "matches filter words on string keys as well" do
33
+ expect(filter_params({"Authorization" => "Bearer x"})).to eq(
34
+ {"Authorization" => 'FILTERED'}
35
+ )
36
+ end
37
+
38
+ it "cuts everything deeper than the configured depth" do
39
+ result = filter_params({a: {b: {c: {d: {e: 1}}}}})
40
+
41
+ expect(result).to eq({a: {b: {c: {d: '[TRUNCATED_DEPTH]'}}}})
42
+ end
43
+
44
+ it "replaces an oversized payload with its size" do
45
+ result = filter_params({blob: "x" * 5000})
46
+
47
+ expect(result[:truncated]).to eq(true)
48
+ expect(result[:size]).to be > 4096
49
+ expect(result[:blob]).to be_nil
50
+ end
51
+
52
+ it "keeps a payload that fits the size limit" do
53
+ result = filter_params({blob: "x" * 100})
54
+
55
+ expect(result[:truncated]).to be_nil
56
+ expect(result[:blob]).to eq("x" * 100)
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ RSpec.configure do |config|
2
+ config.extend Ree::RSpecLinkDSL
3
+ end
4
+
5
+ ENV['AUDIT_ENABLED'] = 'true'
6
+ ENV['AUDIT_MAX_PARAMS_BYTES'] = '4096'
7
+ ENV['AUDIT_MAX_PARAMS_DEPTH'] = '4'
8
+
9
+ # The audit bean links a logger; keep it silent — the specs assert on the
10
+ # logger object itself, not on its output.
11
+ ENV['LOG_FILE_PATH'] = '/tmp/ree_audit.log'
12
+ ENV['LOG_LEVEL_FILE'] = ''
13
+ ENV['LOG_LEVEL_STDOUT'] = ''
@@ -183,21 +183,23 @@ module ReeDao
183
183
  end
184
184
 
185
185
  def with_mapper(mapper)
186
- clone(
187
- schema_mapper: mapper || opts[:schema_mapper],
188
- ).with_row_proc(
186
+ m = mapper || opts[:schema_mapper]
187
+
188
+ row_proc = if m.nil?
189
+ Proc.new { |hash| hash }
190
+ elsif (dto = m.find_strategy(:db_load)&.dto) && dto.include?(ReeDto::DSL)
191
+ # ree_dto entities skip set_persistence_state anyway; avoid the
192
+ # per-row ancestor walk entirely
193
+ Proc.new { |hash| m.db_load(hash) }
194
+ else
189
195
  Proc.new { |hash|
190
- m = mapper || opts[:schema_mapper]
191
-
192
- if m
193
- entity = m.db_load(hash)
194
- self.set_persistence_state(entity, hash)
195
- entity
196
- else
197
- hash
198
- end
196
+ entity = m.db_load(hash)
197
+ self.set_persistence_state(entity, hash)
198
+ entity
199
199
  }
200
- )
200
+ end
201
+
202
+ clone(schema_mapper: m).with_row_proc(row_proc)
201
203
  end
202
204
 
203
205
  private
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ package_require('ree_dao')
4
+ package_require('ree_dto/entity_dsl')
5
+ package_require('ree_dto/dsl')
6
+
7
+ RSpec.describe "db_load performance mode (dao integration)" do
8
+ link :build_sqlite_connection, from: :ree_dao
9
+
10
+ after do
11
+ Ree.disable_irb_mode
12
+ Ree.disable_db_load_performance_mode
13
+ end
14
+
15
+ before :all do
16
+ connection = build_sqlite_connection({database: 'sqlite_db'})
17
+
18
+ if connection.table_exists?(:perf_mode_users)
19
+ connection.drop_table(:perf_mode_users)
20
+ end
21
+
22
+ connection.create_table :perf_mode_users do
23
+ primary_key :id
24
+
25
+ column :name, 'varchar(256)'
26
+ column :age, :integer
27
+ end
28
+
29
+ connection.disconnect
30
+ end
31
+
32
+ Ree.enable_irb_mode
33
+
34
+ module ReeDaoPerfModeTest
35
+ include Ree::PackageDSL
36
+
37
+ package do
38
+ depends_on :ree_dao
39
+ end
40
+ end
41
+
42
+ class ReeDaoPerfModeTest::Db
43
+ include Ree::BeanDSL
44
+
45
+ bean :db do
46
+ singleton
47
+ factory :build
48
+
49
+ link :build_sqlite_connection, from: :ree_dao
50
+ end
51
+
52
+ def build
53
+ build_sqlite_connection({database: 'sqlite_db'})
54
+ end
55
+ end
56
+
57
+ class ReeDaoPerfModeTest::User
58
+ include ReeDto::EntityDSL
59
+
60
+ properties(
61
+ id: Nilor[Integer],
62
+ name: String,
63
+ age: Integer,
64
+ )
65
+
66
+ attr_accessor :name
67
+ end
68
+
69
+ class ReeDaoPerfModeTest::UserDto
70
+ include ReeDto::DSL
71
+
72
+ build_dto do
73
+ field :id, Nilor[Integer]
74
+ field :name, String
75
+ field :age, Integer
76
+ end
77
+ end
78
+
79
+ # mappers are built at class definition time, so the flag state around each
80
+ # class body decides which db_load implementation the dao gets
81
+ Ree.enable_db_load_performance_mode
82
+
83
+ class ReeDaoPerfModeTest::CompiledUsersDao
84
+ include ReeDao::DSL
85
+
86
+ dao :compiled_users_dao do
87
+ link :db
88
+ end
89
+
90
+ table :perf_mode_users
91
+
92
+ schema ReeDaoPerfModeTest::User do
93
+ integer :id, null: true
94
+ string :name
95
+ integer :age
96
+ end
97
+ end
98
+
99
+ class ReeDaoPerfModeTest::CompiledDtoDao
100
+ include ReeDao::DSL
101
+
102
+ dao :compiled_dto_dao do
103
+ link :db
104
+ end
105
+
106
+ table :perf_mode_users
107
+
108
+ schema ReeDaoPerfModeTest::UserDto do
109
+ integer :id, null: true
110
+ string :name
111
+ integer :age
112
+ end
113
+ end
114
+
115
+ Ree.disable_db_load_performance_mode
116
+
117
+ class ReeDaoPerfModeTest::GenericUsersDao
118
+ include ReeDao::DSL
119
+
120
+ dao :generic_users_dao do
121
+ link :db
122
+ end
123
+
124
+ table :perf_mode_users
125
+
126
+ schema ReeDaoPerfModeTest::User do
127
+ integer :id, null: true
128
+ string :name
129
+ integer :age
130
+ end
131
+ end
132
+
133
+ let(:compiled_dao) { ReeDaoPerfModeTest::CompiledUsersDao.new }
134
+ let(:compiled_dto_dao) { ReeDaoPerfModeTest::CompiledDtoDao.new }
135
+ let(:generic_dao) { ReeDaoPerfModeTest::GenericUsersDao.new }
136
+
137
+ it "compiles the schema mapper of daos defined with the mode on" do
138
+ compiled_mapper = ReeDaoPerfModeTest::CompiledUsersDao.instance_variable_get(:@schema_mapper)
139
+ generic_mapper = ReeDaoPerfModeTest::GenericUsersDao.instance_variable_get(:@schema_mapper)
140
+
141
+ expect(compiled_mapper.class.method_defined?(:__generic_db_load, false)).to be true
142
+ expect(generic_mapper.class.method_defined?(:__generic_db_load, false)).to be false
143
+ end
144
+
145
+ it "reads identical entities through compiled and generic daos" do
146
+ generic_dao.delete_all
147
+
148
+ user = ReeDaoPerfModeTest::User.new(name: 'John', age: 30)
149
+ generic_dao.put(user)
150
+
151
+ from_generic = generic_dao.find(user.id)
152
+ from_compiled = compiled_dao.find(user.id)
153
+
154
+ expect(from_compiled.id).to eq(from_generic.id)
155
+ expect(from_compiled.name).to eq(from_generic.name)
156
+ expect(from_compiled.age).to eq(from_generic.age)
157
+
158
+ expect(compiled_dao.all.map(&:id)).to eq(generic_dao.all.map(&:id))
159
+ end
160
+
161
+ it "keeps the update round-trip working for entity_dsl entities" do
162
+ generic_dao.delete_all
163
+
164
+ user = ReeDaoPerfModeTest::User.new(name: 'John', age: 30)
165
+ compiled_dao.put(user)
166
+
167
+ loaded = compiled_dao.find(user.id)
168
+ loaded.name = 'Doe'
169
+ compiled_dao.update(loaded)
170
+
171
+ expect(generic_dao.find(user.id).name).to eq('Doe')
172
+ end
173
+
174
+ it "loads and updates ree_dto entities through the compiled dao" do
175
+ compiled_dto_dao.delete_all
176
+
177
+ dto = ReeDaoPerfModeTest::UserDto.build(id: nil, name: 'Jane', age: 25)
178
+ compiled_dto_dao.put(dto)
179
+
180
+ loaded = compiled_dto_dao.find(dto.id)
181
+ expect(loaded).to be_a(ReeDaoPerfModeTest::UserDto)
182
+ expect(loaded.name).to eq('Jane')
183
+
184
+ loaded.name = 'Janet'
185
+ expect(loaded.changed_fields).to eq([:name])
186
+ compiled_dto_dao.update(loaded)
187
+
188
+ expect(compiled_dto_dao.find(dto.id).name).to eq('Janet')
189
+ end
190
+ end
@@ -32,6 +32,20 @@ module ReeDto::DtoClassMethods
32
32
  dto_obj
33
33
  end
34
34
 
35
+ # Fast-path constructor used by compiled db_load: takes ownership of attrs
36
+ # (it becomes @_attrs without copying), so callers must not reuse the hash.
37
+ # Reads @fields_with_default directly: the contract-wrapped reader would
38
+ # revalidate the array on every row.
39
+ def build_from_attrs(attrs)
40
+ (@fields_with_default || []).each do |field|
41
+ attrs[field.name] = field.default unless attrs.key?(field.name)
42
+ end
43
+
44
+ dto_obj = allocate
45
+ dto_obj.instance_variable_set(:@_attrs, attrs)
46
+ dto_obj
47
+ end
48
+
35
49
  private
36
50
 
37
51
  def set_fields(v)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ package_require("ree_dto/dsl")
3
+
4
+ RSpec.describe "ReeDto build_from_attrs" do
5
+ class ReeDto::BuildFromAttrsDto
6
+ include ReeDto::DSL
7
+
8
+ build_dto do
9
+ field :id, Integer
10
+ field :name, Nilor[String]
11
+ field :with_default, Nilor[Integer], default: 7
12
+ end
13
+ end
14
+
15
+ it "takes ownership of the attrs hash without copying" do
16
+ attrs = { id: 1, name: "n" }
17
+ dto = ReeDto::BuildFromAttrsDto.build_from_attrs(attrs)
18
+
19
+ expect(dto.attrs).to equal(attrs)
20
+ expect(dto.id).to eq(1)
21
+ expect(dto.name).to eq("n")
22
+ end
23
+
24
+ it "fills defaults only for absent keys" do
25
+ dto = ReeDto::BuildFromAttrsDto.build_from_attrs({ id: 1 })
26
+ expect(dto.with_default).to eq(7)
27
+
28
+ dto = ReeDto::BuildFromAttrsDto.build_from_attrs({ id: 1, with_default: nil })
29
+ expect(dto.get_value(:with_default)).to eq(nil)
30
+ end
31
+
32
+ it "matches build for identical inputs" do
33
+ built = ReeDto::BuildFromAttrsDto.build({ id: 1, name: "n" })
34
+ fast = ReeDto::BuildFromAttrsDto.build_from_attrs({ id: 1, name: "n" })
35
+
36
+ expect(fast.attrs).to eq(built.attrs)
37
+ expect(fast).to eq(built)
38
+ end
39
+
40
+ it "produces a fully functional dto" do
41
+ dto = ReeDto::BuildFromAttrsDto.build_from_attrs({ id: 1 })
42
+
43
+ dto.name = "x"
44
+ expect(dto.changed_fields).to eq([:name])
45
+ expect(dto.has_value?(:name)).to be true
46
+ expect(dto.to_h).to eq({ id: 1, name: "x", with_default: 7 })
47
+ end
48
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Compiles a specialized :db_load for composite mappers when
4
+ # Ree.db_load_performance_mode? is on at mapper build time.
5
+ #
6
+ # The compiled method handles the hot call shape `db_load(hash)` with no
7
+ # role/filters as straight-line per-field code: values of primitive types
8
+ # skip casting behind a cheap type guard (see AbstractType#db_load_trusted_check),
9
+ # non-trusted types (custom, enums, pg wrappers, nested mappers) are cast as
10
+ # usual, and dto construction goes through a single attrs hash instead of
11
+ # build-then-copy. Every other call shape falls back to the untouched generic
12
+ # method, kept as __generic_db_load.
13
+ #
14
+ # Codegen targets the mapper's anonymous class (each Mapper.build creates
15
+ # one), never the instance: registered mappers are frozen.
16
+ class ReeMapper::DbLoadCompiler
17
+ class << self
18
+ def compile(mapper)
19
+ return if mapper.type # leaf mappers keep their generic delegation
20
+
21
+ strategy = mapper.find_strategy(:db_load)
22
+ return unless strategy
23
+
24
+ case (output = strategy.output)
25
+ when ReeMapper::ReeDtoOutput
26
+ builder = "acc = {}"
27
+ finisher = "DB_LOAD_DTO.build_from_attrs(acc)"
28
+ when ReeMapper::HashOutput
29
+ return unless strategy.dto == ::Hash # OpenStruct keeps the generic path
30
+ builder = "acc = {}"
31
+ finisher = "acc"
32
+ when ReeMapper::ObjectOutput
33
+ builder = "acc = DB_LOAD_DTO.allocate"
34
+ finisher = "acc"
35
+ else
36
+ return # StructOutput and unknown outputs keep the generic path
37
+ end
38
+
39
+ klass = mapper.class
40
+ return if klass.method_defined?(:__generic_db_load, false) # already compiled
41
+
42
+ fields = mapper.fields.values
43
+
44
+ set_const(klass, :DB_LOAD_FIELDS, fields.freeze)
45
+ set_const(klass, :DB_LOAD_DTO, strategy.dto)
46
+ set_const(
47
+ klass,
48
+ :DB_LOAD_NESTED_FILTERS,
49
+ fields.map { _1.fields_filter ? [_1.fields_filter].freeze : nil }.freeze
50
+ )
51
+
52
+ body = fields.each_with_index.filter_map { |field, idx|
53
+ # the generic loop skips role-scoped fields when role is nil; calls
54
+ # with an actual role go through the generic fallback
55
+ next unless field.roles.empty?
56
+ field_code(field, idx, output, strategy.always_optional)
57
+ }.join("\n")
58
+
59
+ klass.send(:alias_method, :__generic_db_load, :db_load)
60
+
61
+ klass.class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
62
+ def db_load(obj, role: nil, only: nil, except: nil, fields_filters: nil)
63
+ if role || only || except || fields_filters || !obj.is_a?(::Hash)
64
+ return __generic_db_load(obj, role: role, only: only, except: except, fields_filters: fields_filters)
65
+ end
66
+
67
+ #{builder}
68
+ #{body}
69
+ #{finisher}
70
+ end
71
+ RUBY
72
+
73
+ # keep the generated source inspectable for debugging
74
+ klass.instance_variable_set(:@compiled_db_load_source, body)
75
+
76
+ nil
77
+ end
78
+
79
+ private
80
+
81
+ def field_code(field, idx, output, always_optional)
82
+ cast_expr = "__dbl_cast(DB_LOAD_FIELDS[#{idx}], v, DB_LOAD_NESTED_FILTERS[#{idx}])"
83
+ guard = field.type.db_load_trusted_check("v")
84
+
85
+ value_expr = if guard == "true"
86
+ # identity type (any): nil also passes through unchanged, as in the
87
+ # generic loop where Any#db_load(nil) returns nil
88
+ "v"
89
+ elsif guard
90
+ if field.null
91
+ "v.nil? ? nil : (#{guard} ? v : #{cast_expr})"
92
+ else
93
+ "#{guard} ? v : #{cast_expr}"
94
+ end
95
+ else
96
+ if field.null
97
+ "v.nil? ? nil : #{cast_expr}"
98
+ else
99
+ cast_expr
100
+ end
101
+ end
102
+
103
+ sym_key = field.from.inspect
104
+ str_key = field.from_as_str.inspect
105
+
106
+ missing_code = if field.optional || always_optional
107
+ assign(field, output, "DB_LOAD_FIELDS[#{idx}].default") if field.has_default?
108
+ else
109
+ <<~RUBY.chomp
110
+ raise ReeMapper::TypeError.new(
111
+ "is missing (required field)", DB_LOAD_FIELDS[#{idx}].location, [#{str_key}]
112
+ )
113
+ RUBY
114
+ end
115
+
116
+ code = +<<~RUBY
117
+ if obj.key?(#{sym_key})
118
+ v = obj[#{sym_key}]
119
+ #{assign(field, output, value_expr)}
120
+ elsif obj.key?(#{str_key})
121
+ v = obj[#{str_key}]
122
+ #{assign(field, output, value_expr)}
123
+ RUBY
124
+
125
+ if missing_code
126
+ code << "else\n #{missing_code}\n"
127
+ end
128
+
129
+ code << "end\n"
130
+ end
131
+
132
+ def assign(field, output, value_expr)
133
+ if output.is_a?(ReeMapper::ObjectOutput)
134
+ "acc.instance_variable_set(#{field.name_as_instance_var_name.inspect}, #{value_expr})"
135
+ else
136
+ "acc[#{field.name.inspect}] = #{value_expr}"
137
+ end
138
+ end
139
+
140
+ def set_const(klass, name, value)
141
+ klass.send(:remove_const, name) if klass.const_defined?(name, false)
142
+ klass.const_set(name, value)
143
+ end
144
+ end
145
+ end
@@ -139,9 +139,34 @@ class ReeMapper::Mapper
139
139
  def prepare_dto
140
140
  raise ReeMapper::ArgumentError, "mapper should contain at least one field" if fields.empty?
141
141
  strategies.each { _1.prepare_dto(fields.keys) }
142
+
143
+ if ReeMapper.db_load_performance_mode? && find_strategy(:db_load)
144
+ ReeMapper::DbLoadCompiler.compile(self)
145
+ end
146
+
142
147
  nil
143
148
  end
144
149
 
150
+ # DbLoadCompiler helpers below are deliberately contract-free: they run
151
+ # per field per row on the hot path.
152
+
153
+ # Guard expression for the wrapped type, or nil when this mapper's db_load
154
+ # must always be called (composite mappers, wrappers, custom types)
155
+ def db_load_trusted_check(var)
156
+ return nil if @type.nil?
157
+ @type.respond_to?(:db_load_trusted_check) ? @type.db_load_trusted_check(var) : nil
158
+ end
159
+
160
+ # Slow path of compiled db_load: full cast with the same error decoration
161
+ # as the generic per-field loop
162
+ def __dbl_cast(field, value, nested_filters)
163
+ field.type.db_load(value, role: nil, fields_filters: nested_filters)
164
+ rescue ReeMapper::ErrorWithLocation => e
165
+ e.prepend_field_name(field.name_as_str)
166
+ e.location ||= field.location
167
+ raise e
168
+ end
169
+
145
170
  contract(Symbol => Nilor[ReeMapper::MapperStrategy])
146
171
  def find_strategy(strategy_method)
147
172
  strategies.detect { _1.method == strategy_method }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::MapperStrategy
4
- attr_reader :method, :always_optional
4
+ attr_reader :method, :always_optional, :output
5
5
 
6
6
  contract(Symbol, Class, Bool => Any)
7
7
  def initialize(method:, dto:, always_optional:)
@@ -61,8 +61,6 @@ class ReeMapper::MapperStrategy
61
61
 
62
62
  private
63
63
 
64
- attr_reader :output
65
-
66
64
  def build_output(dto)
67
65
  if dto == Hash || (defined?(OpenStruct) && dto == OpenStruct)
68
66
  ReeMapper::HashOutput.new(dto)
@@ -1,4 +1,13 @@
1
1
  class ReeMapper::AbstractType
2
+ # Ruby expression (as source string) checking that `var` already holds a
3
+ # correctly-typed DB value, so db_load may return it as-is. Types that
4
+ # cannot be skipped (custom types, enums, pg wrappers) return nil. The
5
+ # expression must accept exactly the values for which this type's db_load
6
+ # is an identity, or db_load semantics change in performance mode.
7
+ def db_load_trusted_check(var)
8
+ nil
9
+ end
10
+
2
11
  private
3
12
 
4
13
  def truncate(str, limit = 180)
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::Any < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "true"
6
+ end
7
+
4
8
  contract(Any => Any)
5
9
  def serialize(value)
6
10
  value
@@ -4,6 +4,10 @@ class ReeMapper::Bool < ReeMapper::AbstractType
4
4
  TRUE_CAST_VALUES = ['1', 'true', 'on', 1, true].freeze
5
5
  FALSE_CAST_VALUES = ['0', 'false', 'off', 0, false].freeze
6
6
 
7
+ def db_load_trusted_check(var)
8
+ "(true.equal?(#{var}) || false.equal?(#{var}))"
9
+ end
10
+
7
11
  contract(Any => Bool).throws(ReeMapper::TypeError)
8
12
  def serialize(value)
9
13
  if value.is_a?(TrueClass) || value.is_a?(FalseClass)
@@ -3,6 +3,11 @@
3
3
  require 'date'
4
4
 
5
5
  class ReeMapper::Date < ReeMapper::AbstractType
6
+ # instance_of?, not is_a?: DateTime < Date, and cast converts DateTime
7
+ def db_load_trusted_check(var)
8
+ "#{var}.instance_of?(::Date)"
9
+ end
10
+
6
11
  contract(Any => Date).throws(ReeMapper::TypeError)
7
12
  def serialize(value)
8
13
  if value.class == Date
@@ -3,6 +3,10 @@
3
3
  require 'date'
4
4
 
5
5
  class ReeMapper::DateTime < ReeMapper::AbstractType
6
+ def db_load_trusted_check(var)
7
+ "#{var}.instance_of?(::DateTime)"
8
+ end
9
+
6
10
  contract(Any => DateTime).throws(ReeMapper::TypeError)
7
11
  def serialize(value)
8
12
  if value.class == DateTime
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::Float < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "#{var}.is_a?(::Float)"
6
+ end
7
+
4
8
  contract(Any => Float).throws(ReeMapper::TypeError)
5
9
  def serialize(value)
6
10
  if value.is_a?(Float)
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::Integer < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "#{var}.is_a?(::Integer)"
6
+ end
7
+
4
8
  contract(Any => Integer).throws(ReeMapper::TypeError)
5
9
  def serialize(value)
6
10
  if value.is_a? Integer
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::Rational < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "#{var}.is_a?(::Rational)"
6
+ end
7
+
4
8
  contract(Any => Rational).throws(ReeMapper::TypeError)
5
9
  def serialize(value)
6
10
  if value.is_a?(Rational)
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ReeMapper::String < ReeMapper::AbstractType
4
+ def db_load_trusted_check(var)
5
+ "#{var}.is_a?(::String)"
6
+ end
7
+
4
8
  contract(Any => String).throws(ReeMapper::TypeError)
5
9
  def serialize(value)
6
10
  if value.is_a? String
@@ -3,6 +3,11 @@
3
3
  require 'time'
4
4
 
5
5
  class ReeMapper::Time < ReeMapper::AbstractType
6
+ # instance_of?: cast accepts subclasses (e.g. Sequel::SQLTime) only via parse paths
7
+ def db_load_trusted_check(var)
8
+ "#{var}.instance_of?(::Time)"
9
+ end
10
+
6
11
  contract(Any => Time).throws(ReeMapper::TypeError)
7
12
  def serialize(value)
8
13
  if value.class == Time