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
|
@@ -47,12 +47,18 @@ module ReeMapper
|
|
|
47
47
|
|
|
48
48
|
require_relative 'ree_mapper/mapper_strategy'
|
|
49
49
|
require_relative 'ree_mapper/mapper'
|
|
50
|
+
require_relative 'ree_mapper/db_load_compiler'
|
|
50
51
|
require_relative 'ree_mapper/mapper_factory_proxy'
|
|
51
52
|
require_relative 'ree_mapper/mapper_factory'
|
|
52
53
|
|
|
53
54
|
require_relative 'ree_mapper/default_factory'
|
|
54
55
|
require_relative 'ree_mapper/dsl'
|
|
55
56
|
|
|
57
|
+
# Defensive read: an older ree gem without the flag means the mode is off
|
|
58
|
+
def self.db_load_performance_mode?
|
|
59
|
+
Ree.respond_to?(:db_load_performance_mode?) && Ree.db_load_performance_mode?
|
|
60
|
+
end
|
|
61
|
+
|
|
56
62
|
def self.get_mapper_factory(mod)
|
|
57
63
|
if !mod.is_a?(Module)
|
|
58
64
|
raise Ree::Error.new("module should be provided", :invalid_dsl_usage)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'benchmark'
|
|
3
|
+
|
|
4
|
+
package_require "ree_mapper"
|
|
5
|
+
package_require "ree_dto/entity_dsl"
|
|
6
|
+
|
|
7
|
+
# Run with:
|
|
8
|
+
# REE_BENCHMARK=1 bundle exec rspec ... db_load_benchmark_spec.rb
|
|
9
|
+
# Contracts state is fixed at method definition time, so measure both setups
|
|
10
|
+
# in separate processes: a plain run (contracts on) and NO_CONTRACTS=1.
|
|
11
|
+
RSpec.public_send(ENV["REE_BENCHMARK"] ? :describe : :xdescribe, 'db_load performance mode benchmark') do
|
|
12
|
+
link :build_mapper_factory, from: :ree_mapper
|
|
13
|
+
link :build_mapper_strategy, from: :ree_mapper
|
|
14
|
+
|
|
15
|
+
after { Ree.disable_db_load_performance_mode }
|
|
16
|
+
|
|
17
|
+
ROWS_COUNT = 10_000
|
|
18
|
+
REPS = 10
|
|
19
|
+
|
|
20
|
+
class DbLoadBenchDto
|
|
21
|
+
include ReeDto::DSL
|
|
22
|
+
|
|
23
|
+
build_dto do
|
|
24
|
+
field :id, Integer
|
|
25
|
+
field :num2, Integer
|
|
26
|
+
field :num3, Integer
|
|
27
|
+
field :num4, Integer
|
|
28
|
+
field :num5, Integer
|
|
29
|
+
field :name, String
|
|
30
|
+
field :s2, String
|
|
31
|
+
field :s3, String
|
|
32
|
+
field :s4, String
|
|
33
|
+
field :s5, String
|
|
34
|
+
field :active, Bool
|
|
35
|
+
field :flag2, Bool
|
|
36
|
+
field :score, Float
|
|
37
|
+
field :score2, Float
|
|
38
|
+
field :born_on, Date
|
|
39
|
+
field :born2, Date
|
|
40
|
+
field :seen_at, Time
|
|
41
|
+
field :seen2, Time
|
|
42
|
+
field :registered_at, DateTime
|
|
43
|
+
field :payload, Any
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class DbLoadBenchEntity
|
|
48
|
+
include ReeDto::EntityDSL
|
|
49
|
+
|
|
50
|
+
properties(
|
|
51
|
+
id: Nilor[Integer], num2: Nilor[Integer], num3: Nilor[Integer],
|
|
52
|
+
num4: Nilor[Integer], num5: Nilor[Integer],
|
|
53
|
+
name: Nilor[String], s2: Nilor[String], s3: Nilor[String],
|
|
54
|
+
s4: Nilor[String], s5: Nilor[String],
|
|
55
|
+
active: Nilor[Bool], flag2: Nilor[Bool],
|
|
56
|
+
score: Nilor[Float], score2: Nilor[Float],
|
|
57
|
+
born_on: Nilor[Date], born2: Nilor[Date],
|
|
58
|
+
seen_at: Nilor[Time], seen2: Nilor[Time],
|
|
59
|
+
registered_at: Nilor[DateTime], payload: Any,
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
wide_blk = Proc.new do
|
|
64
|
+
integer :id
|
|
65
|
+
integer :num2
|
|
66
|
+
integer :num3
|
|
67
|
+
integer :num4
|
|
68
|
+
integer :num5
|
|
69
|
+
string :name
|
|
70
|
+
string :s2
|
|
71
|
+
string :s3
|
|
72
|
+
string :s4
|
|
73
|
+
string :s5
|
|
74
|
+
bool :active
|
|
75
|
+
bool :flag2
|
|
76
|
+
float :score
|
|
77
|
+
float :score2
|
|
78
|
+
date :born_on
|
|
79
|
+
date :born2
|
|
80
|
+
time :seen_at
|
|
81
|
+
time :seen2
|
|
82
|
+
date_time :registered_at
|
|
83
|
+
any :payload
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
mixed_blk = Proc.new do
|
|
87
|
+
integer :id
|
|
88
|
+
string :name
|
|
89
|
+
bool :active
|
|
90
|
+
date :born_on
|
|
91
|
+
hash :meta, null: true do
|
|
92
|
+
date :day
|
|
93
|
+
integer :num
|
|
94
|
+
end
|
|
95
|
+
array :nums, integer, null: true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_mapper_with_mode(mode_on, dto, blk)
|
|
99
|
+
mode_on ? Ree.enable_db_load_performance_mode : Ree.disable_db_load_performance_mode
|
|
100
|
+
|
|
101
|
+
build_mapper_factory(
|
|
102
|
+
strategies: [
|
|
103
|
+
build_mapper_strategy(method: :db_load, dto: dto, always_optional: true)
|
|
104
|
+
]
|
|
105
|
+
).call.use(:db_load, dto: dto, &blk)
|
|
106
|
+
ensure
|
|
107
|
+
Ree.disable_db_load_performance_mode
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def wide_rows
|
|
111
|
+
Array.new(ROWS_COUNT) do |i|
|
|
112
|
+
{
|
|
113
|
+
id: i, num2: i, num3: i, num4: i, num5: i,
|
|
114
|
+
name: "name-#{i}", s2: "a", s3: "b", s4: "c", s5: "d",
|
|
115
|
+
active: true, flag2: false,
|
|
116
|
+
score: 1.5, score2: 2.5,
|
|
117
|
+
born_on: Date.new(2020, 1, 1), born2: Date.new(2021, 2, 2),
|
|
118
|
+
seen_at: Time.at(i).utc, seen2: Time.at(i * 2).utc,
|
|
119
|
+
registered_at: DateTime.new(2020, 1, 1, 12, 0, 0),
|
|
120
|
+
payload: { x: i },
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def mixed_rows
|
|
126
|
+
Array.new(ROWS_COUNT) do |i|
|
|
127
|
+
{
|
|
128
|
+
id: i, name: "name-#{i}", active: true, born_on: Date.new(2020, 1, 1),
|
|
129
|
+
meta: { day: "2020-01-01", num: i }, # string date: the JSONB guard-fallback path
|
|
130
|
+
nums: [1, 2, 3],
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def report_alloc(label, rows, mapper)
|
|
136
|
+
GC.start
|
|
137
|
+
allocated_before = GC.stat(:total_allocated_objects)
|
|
138
|
+
gc_before = GC.count
|
|
139
|
+
rows.each { mapper.db_load(_1) }
|
|
140
|
+
allocated = GC.stat(:total_allocated_objects) - allocated_before
|
|
141
|
+
gc_runs = GC.count - gc_before
|
|
142
|
+
puts format("%-28s %10d objects, %3d GC runs per %d rows", label, allocated, gc_runs, rows.size)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it 'wide primitive row: generic vs compiled' do
|
|
146
|
+
rows = wide_rows
|
|
147
|
+
|
|
148
|
+
pairs = {
|
|
149
|
+
"hash dto" => [
|
|
150
|
+
build_mapper_with_mode(false, Hash, wide_blk),
|
|
151
|
+
build_mapper_with_mode(true, Hash, wide_blk),
|
|
152
|
+
],
|
|
153
|
+
"ree_dto" => [
|
|
154
|
+
build_mapper_with_mode(false, DbLoadBenchDto, wide_blk),
|
|
155
|
+
build_mapper_with_mode(true, DbLoadBenchDto, wide_blk),
|
|
156
|
+
],
|
|
157
|
+
"entity_dsl" => [
|
|
158
|
+
build_mapper_with_mode(false, DbLoadBenchEntity, wide_blk),
|
|
159
|
+
build_mapper_with_mode(true, DbLoadBenchEntity, wide_blk),
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
puts "\ncontracts: #{Ree::Contracts.no_contracts? ? 'OFF (NO_CONTRACTS)' : 'ON'}"
|
|
164
|
+
|
|
165
|
+
Benchmark.bmbm do |x|
|
|
166
|
+
pairs.each do |name, (generic, compiled)|
|
|
167
|
+
x.report("generic #{name}") { REPS.times { rows.each { generic.db_load(_1) } } }
|
|
168
|
+
x.report("compiled #{name}") { REPS.times { rows.each { compiled.db_load(_1) } } }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
puts "\nallocations:"
|
|
173
|
+
pairs.each do |name, (generic, compiled)|
|
|
174
|
+
report_alloc("generic #{name}", rows, generic)
|
|
175
|
+
report_alloc("compiled #{name}", rows, compiled)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it 'mixed row with nested hash and array: generic vs compiled' do
|
|
180
|
+
rows = mixed_rows
|
|
181
|
+
generic = build_mapper_with_mode(false, DbLoadBenchDto, mixed_blk)
|
|
182
|
+
compiled = build_mapper_with_mode(true, DbLoadBenchDto, mixed_blk)
|
|
183
|
+
|
|
184
|
+
Benchmark.bmbm do |x|
|
|
185
|
+
x.report("generic mixed") { REPS.times { rows.each { generic.db_load(_1) } } }
|
|
186
|
+
x.report("compiled mixed") { REPS.times { rows.each { compiled.db_load(_1) } } }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
puts "\nallocations:"
|
|
190
|
+
report_alloc("generic mixed", rows, generic)
|
|
191
|
+
report_alloc("compiled mixed", rows, compiled)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Results (2026-08-16, Ruby 4.0.1, 10_000 rows x 10 reps, real seconds):
|
|
196
|
+
#
|
|
197
|
+
# contracts: ON
|
|
198
|
+
# generic compiled speedup
|
|
199
|
+
# wide (20 fields) hash 60.53s 0.41s ~149x
|
|
200
|
+
# wide ree_dto 74.76s 0.43s ~174x
|
|
201
|
+
# wide entity_dsl 63.78s 0.36s ~175x
|
|
202
|
+
# mixed (jsonb-like) 37.23s 5.43s ~6.9x
|
|
203
|
+
#
|
|
204
|
+
# allocations per 10_000 rows:
|
|
205
|
+
# wide hash 15_820_000 -> 10_000 wide ree_dto 18_350_000 -> 20_000
|
|
206
|
+
# wide entity_dsl 15_820_000 -> 10_000 mixed 8_930_000 -> 1_190_000
|
|
207
|
+
# GC runs: 40-46 -> 0 (wide), 72 -> 9 (mixed)
|
|
208
|
+
#
|
|
209
|
+
# contracts: OFF (NO_CONTRACTS=1)
|
|
210
|
+
# generic compiled speedup
|
|
211
|
+
# wide (20 fields) hash 2.54s 0.42s ~6.0x
|
|
212
|
+
# wide ree_dto 2.77s 0.43s ~6.4x
|
|
213
|
+
# wide entity_dsl 2.43s 0.39s ~6.2x
|
|
214
|
+
# mixed (jsonb-like) 2.52s 1.21s ~2.1x
|
|
215
|
+
#
|
|
216
|
+
# allocations per 10_000 rows:
|
|
217
|
+
# wide hash 220_000 -> 10_000 wide ree_dto 260_000 -> 20_000
|
|
218
|
+
# wide entity_dsl 220_000 -> 10_000 mixed 360_000 -> 200_000
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
package_require "ree_mapper"
|
|
3
|
+
package_require "ree_dto/entity_dsl"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "db_load performance mode" do
|
|
6
|
+
link :build_mapper_factory, from: :ree_mapper
|
|
7
|
+
link :build_mapper_strategy, from: :ree_mapper
|
|
8
|
+
|
|
9
|
+
after do
|
|
10
|
+
Ree.disable_db_load_performance_mode
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class PerfModeDto
|
|
14
|
+
include ReeDto::DSL
|
|
15
|
+
|
|
16
|
+
build_dto do
|
|
17
|
+
field :id, Integer
|
|
18
|
+
field :name, String
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class PerfModeEntity
|
|
23
|
+
include ReeDto::EntityDSL
|
|
24
|
+
|
|
25
|
+
properties(
|
|
26
|
+
id: Nilor[Integer],
|
|
27
|
+
name: Nilor[String],
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_factory(dto:, always_optional: true)
|
|
32
|
+
build_mapper_factory(
|
|
33
|
+
strategies: [
|
|
34
|
+
build_mapper_strategy(method: :db_load, dto: dto, always_optional: always_optional)
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# builds the same mapper twice: with the mode off (generic path) and on
|
|
40
|
+
# (compiled path), so examples can assert identical behavior
|
|
41
|
+
def build_pair(dto: Hash, always_optional: true, register_as: nil, &blk)
|
|
42
|
+
Ree.disable_db_load_performance_mode
|
|
43
|
+
generic = build_factory(dto: dto, always_optional: always_optional)
|
|
44
|
+
.call(register_as: register_as).use(:db_load, dto: dto, &blk)
|
|
45
|
+
|
|
46
|
+
Ree.enable_db_load_performance_mode
|
|
47
|
+
compiled = build_factory(dto: dto, always_optional: always_optional)
|
|
48
|
+
.call(register_as: register_as).use(:db_load, dto: dto, &blk)
|
|
49
|
+
Ree.disable_db_load_performance_mode
|
|
50
|
+
|
|
51
|
+
[generic, compiled]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def expect_same_result(generic, compiled, input, **opts)
|
|
55
|
+
generic_result = begin; generic.db_load(input, **opts); rescue => e; e; end
|
|
56
|
+
compiled_result = begin; compiled.db_load(input, **opts); rescue => e; e; end
|
|
57
|
+
|
|
58
|
+
if generic_result.is_a?(Exception)
|
|
59
|
+
expect(compiled_result.class).to eq(generic_result.class)
|
|
60
|
+
expect(compiled_result.message).to eq(generic_result.message)
|
|
61
|
+
else
|
|
62
|
+
expect(compiled_result).to eq(generic_result)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
compiled_result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
wide_blk = Proc.new do
|
|
69
|
+
integer :id
|
|
70
|
+
string :name
|
|
71
|
+
bool :active
|
|
72
|
+
float :score, null: true
|
|
73
|
+
date :born_on, null: true
|
|
74
|
+
time :seen_at, null: true
|
|
75
|
+
date_time :registered_at, null: true
|
|
76
|
+
rational :ratio, null: true
|
|
77
|
+
any :payload, null: true
|
|
78
|
+
integer :renamed, from: :src
|
|
79
|
+
string? :note, default: "-"
|
|
80
|
+
integer? :maybe
|
|
81
|
+
integer :admin_count, role: :admin
|
|
82
|
+
hash :meta, null: true do
|
|
83
|
+
date :day
|
|
84
|
+
integer :num
|
|
85
|
+
end
|
|
86
|
+
array :nums, integer, null: true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def wide_row
|
|
90
|
+
{
|
|
91
|
+
id: 1,
|
|
92
|
+
name: "n",
|
|
93
|
+
active: true,
|
|
94
|
+
score: 1.5,
|
|
95
|
+
born_on: Date.new(2020, 1, 1),
|
|
96
|
+
seen_at: Time.at(0).utc,
|
|
97
|
+
registered_at: DateTime.new(2020, 1, 1, 12, 0, 0),
|
|
98
|
+
ratio: Rational(1, 2),
|
|
99
|
+
payload: { x: 1 },
|
|
100
|
+
src: 5,
|
|
101
|
+
note: "hi",
|
|
102
|
+
maybe: 2,
|
|
103
|
+
meta: { day: "2020-01-01", num: 3 },
|
|
104
|
+
nums: [1, 2],
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "compiles db_load when the mode is on and keeps the generic fallback" do
|
|
109
|
+
_generic, compiled = build_pair(&wide_blk)
|
|
110
|
+
|
|
111
|
+
expect(compiled.class.method_defined?(:__generic_db_load, false)).to be true
|
|
112
|
+
expect(compiled.class.const_defined?(:DB_LOAD_FIELDS, false)).to be true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "does not compile when the mode is off" do
|
|
116
|
+
generic, _compiled = build_pair(&wide_blk)
|
|
117
|
+
|
|
118
|
+
expect(generic.class.method_defined?(:__generic_db_load, false)).to be false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
context "parity with the generic path" do
|
|
122
|
+
it "loads a full row identically" do
|
|
123
|
+
generic, compiled = build_pair(&wide_blk)
|
|
124
|
+
result = expect_same_result(generic, compiled, wide_row)
|
|
125
|
+
|
|
126
|
+
expect(result[:id]).to eq(1)
|
|
127
|
+
expect(result[:renamed]).to eq(5)
|
|
128
|
+
expect(result[:meta][:day]).to eq(Date.new(2020, 1, 1)) # JSONB-style string is still cast
|
|
129
|
+
expect(result[:nums]).to eq([1, 2])
|
|
130
|
+
expect(result.key?(:admin_count)).to be false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "still coerces values that fail the type guard" do
|
|
134
|
+
generic, compiled = build_pair(&wide_blk)
|
|
135
|
+
result = expect_same_result(generic, compiled, wide_row.merge(id: "5", score: 2))
|
|
136
|
+
|
|
137
|
+
expect(result[:id]).to eq(5)
|
|
138
|
+
expect(result[:score]).to eq(2.0)
|
|
139
|
+
expect(result[:score]).to be_a(Float)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "raises the same error for nil in a null: false field" do
|
|
143
|
+
generic, compiled = build_pair(&wide_blk)
|
|
144
|
+
error = expect_same_result(generic, compiled, wide_row.merge(name: nil))
|
|
145
|
+
|
|
146
|
+
expect(error).to be_a(ReeMapper::TypeError)
|
|
147
|
+
expect(error.message).to include("`name`")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "raises the same error for garbage that fails coercion" do
|
|
151
|
+
generic, compiled = build_pair(&wide_blk)
|
|
152
|
+
error = expect_same_result(generic, compiled, wide_row.merge(id: "abc"))
|
|
153
|
+
|
|
154
|
+
expect(error).to be_a(ReeMapper::CoercionError)
|
|
155
|
+
expect(error.message).to include("`id`")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "raises the same error from a nested mapper with the field path" do
|
|
159
|
+
generic, compiled = build_pair(&wide_blk)
|
|
160
|
+
error = expect_same_result(generic, compiled, wide_row.merge(meta: { day: "not a date", num: 1 }))
|
|
161
|
+
|
|
162
|
+
expect(error).to be_a(ReeMapper::CoercionError)
|
|
163
|
+
expect(error.message).to include("`meta[day]`")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "assigns nil without casting for null: true fields" do
|
|
167
|
+
generic, compiled = build_pair(&wide_blk)
|
|
168
|
+
result = expect_same_result(
|
|
169
|
+
generic, compiled,
|
|
170
|
+
wide_row.merge(score: nil, born_on: nil, meta: nil, nums: nil, payload: nil)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
expect(result[:score]).to be_nil
|
|
174
|
+
expect(result[:meta]).to be_nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "applies defaults and skips missing optional fields" do
|
|
178
|
+
generic, compiled = build_pair(&wide_blk)
|
|
179
|
+
row = wide_row
|
|
180
|
+
row.delete(:note)
|
|
181
|
+
row.delete(:maybe)
|
|
182
|
+
result = expect_same_result(generic, compiled, row)
|
|
183
|
+
|
|
184
|
+
expect(result[:note]).to eq("-")
|
|
185
|
+
expect(result.key?(:maybe)).to be false
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it "skips missing fields when the strategy is always_optional" do
|
|
189
|
+
generic, compiled = build_pair(&wide_blk)
|
|
190
|
+
result = expect_same_result(generic, compiled, { id: 1 })
|
|
191
|
+
|
|
192
|
+
expect(result).to eq({ id: 1, note: "-" })
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "raises for missing required fields when the strategy is not always_optional" do
|
|
196
|
+
generic, compiled = build_pair(always_optional: false) do
|
|
197
|
+
integer :id
|
|
198
|
+
string? :name
|
|
199
|
+
end
|
|
200
|
+
error = expect_same_result(generic, compiled, { name: "n" })
|
|
201
|
+
|
|
202
|
+
expect(error).to be_a(ReeMapper::TypeError)
|
|
203
|
+
expect(error.message).to include("`id` is missing (required field)")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "supports string-keyed input" do
|
|
207
|
+
generic, compiled = build_pair(&wide_blk)
|
|
208
|
+
result = expect_same_result(generic, compiled, { "id" => 1, "name" => "n" })
|
|
209
|
+
|
|
210
|
+
expect(result[:id]).to eq(1)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "falls back to the generic path for non-Hash input" do
|
|
214
|
+
generic, compiled = build_pair(&wide_blk)
|
|
215
|
+
input = OpenStruct.new(id: 1, name: "n")
|
|
216
|
+
result = expect_same_result(generic, compiled, input)
|
|
217
|
+
|
|
218
|
+
expect(result[:id]).to eq(1)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "falls back to the generic path for role calls" do
|
|
222
|
+
generic, compiled = build_pair(&wide_blk)
|
|
223
|
+
result = expect_same_result(generic, compiled, wide_row.merge(admin_count: 7), role: :admin)
|
|
224
|
+
|
|
225
|
+
expect(result[:admin_count]).to eq(7)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "falls back to the generic path for only/except calls" do
|
|
229
|
+
generic, compiled = build_pair(&wide_blk)
|
|
230
|
+
result = expect_same_result(generic, compiled, wide_row, only: [:id, :name])
|
|
231
|
+
|
|
232
|
+
expect(result.keys).to contain_exactly(:id, :name)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "respects field-level only filters on nested mappers" do
|
|
236
|
+
generic, compiled = build_pair do
|
|
237
|
+
integer :id
|
|
238
|
+
hash :meta, null: true, only: [:num] do
|
|
239
|
+
date :day, null: true
|
|
240
|
+
integer :num
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
result = expect_same_result(generic, compiled, { id: 1, meta: { day: "2020-01-01", num: 3 } })
|
|
244
|
+
|
|
245
|
+
expect(result[:meta]).to eq({ num: 3 })
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
context "dto outputs" do
|
|
250
|
+
it "builds ree_dto entities" do
|
|
251
|
+
generic, compiled = build_pair(dto: PerfModeDto) do
|
|
252
|
+
integer :id
|
|
253
|
+
string :name
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
generic_dto = generic.db_load({ id: 1, name: "n" })
|
|
257
|
+
compiled_dto = compiled.db_load({ id: 1, name: "n" })
|
|
258
|
+
|
|
259
|
+
expect(compiled_dto).to be_a(PerfModeDto)
|
|
260
|
+
expect(compiled_dto.id).to eq(generic_dto.id)
|
|
261
|
+
expect(compiled_dto.name).to eq(generic_dto.name)
|
|
262
|
+
expect(compiled_dto.attrs).to eq(generic_dto.attrs)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "builds entity_dsl objects via instance variables" do
|
|
266
|
+
generic, compiled = build_pair(dto: PerfModeEntity) do
|
|
267
|
+
integer :id
|
|
268
|
+
string :name
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
generic_entity = generic.db_load({ id: 1, name: "n" })
|
|
272
|
+
compiled_entity = compiled.db_load({ id: 1, name: "n" })
|
|
273
|
+
|
|
274
|
+
expect(compiled_entity).to be_a(PerfModeEntity)
|
|
275
|
+
expect(compiled_entity.id).to eq(generic_entity.id)
|
|
276
|
+
expect(compiled_entity.name).to eq(generic_entity.name)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it "keeps the generic path for struct dto" do
|
|
280
|
+
Ree.enable_db_load_performance_mode
|
|
281
|
+
mapper = build_factory(dto: Struct).call.use(:db_load, dto: Struct) do
|
|
282
|
+
integer :id
|
|
283
|
+
end
|
|
284
|
+
Ree.disable_db_load_performance_mode
|
|
285
|
+
|
|
286
|
+
expect(mapper.class.method_defined?(:__generic_db_load, false)).to be false
|
|
287
|
+
expect(mapper.db_load({ id: 1 }).id).to eq(1)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
context "registered (frozen) mappers" do
|
|
292
|
+
it "compiles mappers registered via register_as" do
|
|
293
|
+
generic, compiled = build_pair(register_as: :perf_mode_frozen, &wide_blk)
|
|
294
|
+
|
|
295
|
+
expect(compiled.frozen?).to be true
|
|
296
|
+
expect(compiled.class.method_defined?(:__generic_db_load, false)).to be true
|
|
297
|
+
expect_same_result(generic, compiled, wide_row)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
describe "guard/cast identity invariant" do
|
|
302
|
+
{
|
|
303
|
+
ReeMapper::Integer => [1, -5, 10**20],
|
|
304
|
+
ReeMapper::String => ["s", ""],
|
|
305
|
+
ReeMapper::Bool => [true, false],
|
|
306
|
+
ReeMapper::Float => [1.5, -0.0],
|
|
307
|
+
ReeMapper::Date => [Date.new(2020, 1, 1)],
|
|
308
|
+
ReeMapper::Time => [Time.at(0).utc],
|
|
309
|
+
ReeMapper::DateTime => [DateTime.new(2020, 1, 1)],
|
|
310
|
+
ReeMapper::Rational => [Rational(1, 2)],
|
|
311
|
+
ReeMapper::Any => [1, "s", nil, [1], { a: 1 }],
|
|
312
|
+
}.each do |type_class, values|
|
|
313
|
+
it "#{type_class}: guard implies db_load is identity" do
|
|
314
|
+
type = type_class.new
|
|
315
|
+
check = type.db_load_trusted_check("v")
|
|
316
|
+
|
|
317
|
+
values.each do |v|
|
|
318
|
+
expect(eval(check)).to be(true), "guard rejected #{v.inspect} for #{type_class}"
|
|
319
|
+
expect(type.db_load(v)).to equal(v)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "guards reject values that db_load would convert" do
|
|
325
|
+
{
|
|
326
|
+
ReeMapper::Integer.new => "5",
|
|
327
|
+
ReeMapper::Float.new => 2,
|
|
328
|
+
ReeMapper::Date.new => DateTime.new(2020, 1, 1),
|
|
329
|
+
ReeMapper::Time.new => DateTime.new(2020, 1, 1),
|
|
330
|
+
ReeMapper::DateTime.new => Time.at(0).utc,
|
|
331
|
+
ReeMapper::Rational.new => "1/2",
|
|
332
|
+
ReeMapper::Bool.new => 1,
|
|
333
|
+
}.each do |type, v|
|
|
334
|
+
check = type.db_load_trusted_check("v")
|
|
335
|
+
expect(eval(check)).to be(false), "guard accepted #{v.inspect} for #{type.class}"
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -7,12 +7,16 @@ class Roda
|
|
|
7
7
|
package_require("ree_json/functions/to_json")
|
|
8
8
|
package_require("ree_hash/functions/transform_values")
|
|
9
9
|
package_require("ree_object/functions/not_blank")
|
|
10
|
+
package_require("ree_audit/beans/audit")
|
|
11
|
+
package_require("ree_audit/event")
|
|
10
12
|
|
|
11
13
|
app.plugin :all_verbs
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def self.configure(app, opts = {})
|
|
15
17
|
app.opts[:ree_routes_before] = opts[:before] if opts[:before]
|
|
18
|
+
# An application that passes no `audit:` behaves exactly as before.
|
|
19
|
+
app.opts[:ree_routes_audit] = opts[:audit] if opts[:audit]
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
module ClassMethods
|
|
@@ -126,7 +130,31 @@ class Roda
|
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
accessor = authenticated_user
|
|
129
|
-
|
|
133
|
+
audit = context.opts[:ree_routes_audit]
|
|
134
|
+
|
|
135
|
+
action_result = if audit && route.audited?
|
|
136
|
+
# The event carries the params exactly as the action
|
|
137
|
+
# received them. IP, user agent and session come from the
|
|
138
|
+
# accessor rather than from the request, so that the audit
|
|
139
|
+
# package stays free of any Roda dependency.
|
|
140
|
+
audit_event = ReeAudit::Event.build(
|
|
141
|
+
action_name: route.action.name,
|
|
142
|
+
package_name: route.action.package_name,
|
|
143
|
+
summary: route.summary,
|
|
144
|
+
sections: route.sections,
|
|
145
|
+
request_method: route.request_method,
|
|
146
|
+
path: route.path,
|
|
147
|
+
request_path: r.path,
|
|
148
|
+
params: filtered_params,
|
|
149
|
+
accessor: accessor
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
audit.around(audit_event) do
|
|
153
|
+
get_cached_action(route).call(accessor, filtered_params)
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
get_cached_action(route).call(accessor, filtered_params)
|
|
157
|
+
end
|
|
130
158
|
|
|
131
159
|
if route.serializer
|
|
132
160
|
serialized_result = get_cached_serializer(route).serialize(action_result)
|