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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +2 -2
- data/lib/ree_lib/Packages.schema.json +4 -0
- data/lib/ree_lib/packages/ree_audit/.rspec +2 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/beans/audit.rb +83 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/config.rb +62 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/context.rb +44 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/event.rb +78 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/functions/filter_params.rb +75 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/sinks/logger_sink.rb +15 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/sinks/null_sink.rb +11 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit/sinks/sink.rb +10 -0
- data/lib/ree_lib/packages/ree_audit/package/ree_audit.rb +37 -0
- data/lib/ree_lib/packages/ree_audit/spec/ree_audit/beans/audit_spec.rb +161 -0
- data/lib/ree_lib/packages/ree_audit/spec/ree_audit/context_spec.rb +47 -0
- data/lib/ree_lib/packages/ree_audit/spec/ree_audit/functions/filter_params_spec.rb +58 -0
- data/lib/ree_lib/packages/ree_audit/spec/spec_helper.rb +13 -0
- data/lib/ree_lib/packages/ree_dao/package/ree_dao/dataset_extensions.rb +15 -13
- data/lib/ree_lib/packages/ree_dao/spec/ree_dao/db_load_performance_mode_spec.rb +190 -0
- data/lib/ree_lib/packages/ree_dto/package/ree_dto/dto/dto_class_methods.rb +14 -0
- data/lib/ree_lib/packages/ree_dto/spec/ree_dto/build_from_attrs_spec.rb +48 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/db_load_compiler.rb +145 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/mapper.rb +25 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/mapper_strategy.rb +1 -3
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/abstract_type.rb +9 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/any.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/bool.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/date.rb +5 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/date_time.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/float.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/integer.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/rational.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/string.rb +4 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/types/time.rb +5 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper/wrappers/abstract_wrapper.rb +5 -0
- data/lib/ree_lib/packages/ree_mapper/package/ree_mapper.rb +6 -0
- data/lib/ree_lib/packages/ree_mapper/spec/ree_mapper/benchmarks/db_load_benchmark_spec.rb +218 -0
- data/lib/ree_lib/packages/ree_mapper/spec/ree_mapper/db_load_performance_mode_spec.rb +339 -0
- data/lib/ree_lib/packages/ree_roda/package/ree_roda/plugins/ree_routes.rb +29 -1
- data/lib/ree_lib/packages/ree_roda/package/ree_roda.rb +1 -0
- data/lib/ree_lib/packages/ree_roda/spec/ree_roda/plugins/ree_routes_audit_spec.rb +281 -0
- data/lib/ree_lib/packages/ree_roda/spec/spec_helper.rb +3 -0
- data/lib/ree_lib/packages/ree_routes/package/ree_routes/route.rb +11 -1
- data/lib/ree_lib/packages/ree_routes/package/ree_routes/route_builder.rb +7 -0
- data/lib/ree_lib/packages/ree_routes/spec/ree_routes/dsl_spec.rb +43 -0
- data/lib/ree_lib/version.rb +1 -1
- 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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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)
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -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
|