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
@@ -6,6 +6,11 @@ class ReeMapper::AbstractWrapper
6
6
  @subject = field
7
7
  end
8
8
 
9
+ # See ReeMapper::AbstractType#db_load_trusted_check. Wrappers always cast.
10
+ def db_load_trusted_check(var)
11
+ nil
12
+ end
13
+
9
14
  private
10
15
 
11
16
  def truncate(str, limit = 180)
@@ -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
- action_result = get_cached_action(route).call(accessor, filtered_params)
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)
@@ -11,6 +11,7 @@ module ReeRoda
11
11
  depends_on :ree_object
12
12
  depends_on :ree_swagger
13
13
  depends_on :ree_errors
14
+ depends_on :ree_audit
14
15
  end
15
16
  end
16
17