foobara 0.0.118 → 0.0.120

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 (25) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/projects/command_connectors/src/command_connector.rb +15 -4
  4. data/projects/command_connectors/src/command_registry.rb +9 -3
  5. data/projects/command_connectors/src/serializers/aggregate_serializer.rb +2 -2
  6. data/projects/command_connectors/src/serializers/entities_to_primary_keys_serializer.rb +2 -2
  7. data/projects/command_connectors/src/transformed_command.rb +151 -80
  8. data/projects/detached_entity/src/concerns/equality.rb +2 -0
  9. data/projects/model/src/sensitive_type_removers/model.rb +22 -5
  10. data/projects/model/src/sensitive_value_removers/model.rb +5 -1
  11. data/projects/namespace/src/is_namespace.rb +6 -0
  12. data/projects/namespace/src/namespace_helpers.rb +30 -3
  13. data/projects/persistence/src/entity_base/transaction/concerns/entity_callback_handling.rb +2 -0
  14. data/projects/persistence/src/entity_base/transaction_table/concerns/queries.rb +2 -3
  15. data/projects/persistence/src/entity_base/transaction_table/concerns/record_tracking.rb +2 -2
  16. data/projects/persistence/src/entity_base/transaction_table.rb +38 -13
  17. data/projects/type_declarations/lib/foobara/type_declarations.rb +3 -0
  18. data/projects/type_declarations/src/attributes_transformers/only.rb +12 -0
  19. data/projects/type_declarations/src/attributes_transformers/reject.rb +12 -0
  20. data/projects/type_declarations/src/attributes_transformers.rb +11 -0
  21. data/projects/type_declarations/src/remove_sensitive_values_transformer.rb +25 -11
  22. data/projects/type_declarations/src/typed_transformer.rb +1 -1
  23. data/projects/value/src/processor.rb +6 -0
  24. data/projects/weak_object_set/src/weak_object_set.rb +164 -93
  25. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d533ff82e3a7cfc662c86200e72348deba90893cf323bcb659d15ad0bceaeb63
4
- data.tar.gz: d0dd96e29ac80ffefcb0c815e84d740e783485f41f9dbec60036fbac4d6e2dc3
3
+ metadata.gz: df43aec79b46e3e71fd064c6482bdd5dd6e0af495987d6cea1cdf97311460e91
4
+ data.tar.gz: 800093b7a126bbca27fa300c4df50b568b329c762ad7a2c68925f4a3d5690090
5
5
  SHA512:
6
- metadata.gz: a52322e2a4c15b64c334b4dc6e25dc77be9424628f314c453398fb98de114e96c596c1812250f607e408764342bed0a9062f250d1674328c44b00f4cc72eb7d4
7
- data.tar.gz: f193c28f3db9a9a484661d51b33626ebcaeaa6e1621e07b978f21986fcbf91c8ecd5e21b5fdb7984362a80e0449c9f7292f4c7684331321eb4bb6c0a03ff599b
6
+ metadata.gz: 6305514bda86178efb70d09cd8ff8d46e404f0407b50189833b9939230cf716a80049afca17ea5071599bfb71f4cd45a660ade570b6bfcec063190e9b3a02096
7
+ data.tar.gz: 84705a7ff3bd306c13e7cdf5e81a3a960b0e769df51dacf38859a003da408926133c434d9d03ba2a58b70e57946b58d8355da338a0e780cab34f35a3e84df97c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ # [0.0.120] - 2025-05-11
2
+
3
+ - Fix a race condition in persistence
4
+ - Fix various problems preventing sensitive/private types from being properly removed
5
+ in exposed types
6
+
7
+ # [0.0.119] - 2025-05-08
8
+
9
+ - Make anonymous transformers more deterministic in manifests
10
+ - Remove delegates and private attributes from exposed models
11
+ - Make sure delegated attributes make it out through aggregate serializers
12
+
13
+ # [0.0.118] - 2025-05-07
14
+
15
+ - Fix bug preventing allow_nil from being used with entities
16
+ - Fix bugs with .all and .load
17
+ - Fix bug with possible error class lookups for tuple types
18
+
1
19
  # [0.0.117] - 2025-05-05
2
20
 
3
21
  - Make sure we do not apply an authenticator unless it is applicable
@@ -454,6 +454,13 @@ module Foobara
454
454
  end
455
455
 
456
456
  def foobara_manifest
457
+ Namespace.use command_registry do
458
+ foobara_manifest_in_current_namespace
459
+ end
460
+ end
461
+
462
+ # TODO: try to break this giant method up
463
+ def foobara_manifest_in_current_namespace
457
464
  process_delayed_connections
458
465
 
459
466
  to_include = Set.new
@@ -506,16 +513,20 @@ module Foobara
506
513
  else
507
514
  domain_name = o.foobara_domain.scoped_full_name
508
515
 
509
- unless command_registry.foobara_registered?(domain_name)
516
+ unless command_registry.foobara_registered?(domain_name, mode: Namespace::LookupMode::ABSOLUTE)
510
517
  command_registry.build_and_register_exposed_domain(domain_name)
511
518
 
512
519
  # Since we don't know which other domains/orgs creating this domain might have created,
513
520
  # we will just add them all to be included just in case
514
- command_registry.foobara_all_domain.each do |exposed_domain|
521
+ command_registry.foobara_all_domain(
522
+ mode: Namespace::LookupMode::ABSOLUTE
523
+ ).each do |exposed_domain|
515
524
  additional_to_include << exposed_domain
516
525
  end
517
526
 
518
- command_registry.foobara_all_organization.each do |exposed_organization|
527
+ command_registry.foobara_all_organization(
528
+ mode: Namespace::LookupMode::ABSOLUTE
529
+ ).each do |exposed_organization|
519
530
  additional_to_include << exposed_organization
520
531
  end
521
532
  end
@@ -612,7 +623,7 @@ module Foobara
612
623
  def all_exposed_commands
613
624
  process_delayed_connections
614
625
 
615
- command_registry.foobara_all_command
626
+ command_registry.foobara_all_command(mode: Namespace::LookupMode::ABSOLUTE)
616
627
  end
617
628
  end
618
629
  end
@@ -24,6 +24,8 @@ module Foobara
24
24
  end
25
25
 
26
26
  foobara_add_category_for_instance_of(:command, ExposedCommand)
27
+
28
+ foobara_depends_on_namespaces << Namespace.global
27
29
  end
28
30
 
29
31
  def register(command_class, **)
@@ -32,7 +34,8 @@ module Foobara
32
34
 
33
35
  def create_exposed_command(command_class, **)
34
36
  full_domain_name = command_class.domain.scoped_full_name
35
- exposed_domain = foobara_lookup_domain(full_domain_name) || build_and_register_exposed_domain(full_domain_name)
37
+ exposed_domain = foobara_lookup_domain(full_domain_name, mode: Namespace::LookupMode::ABSOLUTE) ||
38
+ build_and_register_exposed_domain(full_domain_name)
36
39
 
37
40
  exposed_command = create_exposed_command_without_domain(command_class, **)
38
41
 
@@ -75,8 +78,11 @@ module Foobara
75
78
  end
76
79
 
77
80
  full_organization_name = domain_module.foobara_full_organization_name
78
- exposed_organization = foobara_lookup_organization(full_organization_name) ||
79
- build_and_register_exposed_organization(full_organization_name)
81
+
82
+ exposed_organization = foobara_lookup_organization(
83
+ full_organization_name,
84
+ mode: Namespace::LookupMode::ABSOLUTE
85
+ ) || build_and_register_exposed_organization(full_organization_name)
80
86
 
81
87
  exposed_domain = Module.new
82
88
  exposed_domain.foobara_namespace!
@@ -11,9 +11,9 @@ module Foobara
11
11
  object.class.load(object)
12
12
  end
13
13
 
14
- transform(object.attributes)
14
+ transform(object.attributes_with_delegates)
15
15
  when Model
16
- transform(object.attributes)
16
+ transform(object.attributes_with_delegates)
17
17
  when Array
18
18
  object.map { |element| transform(element) }
19
19
  when Hash
@@ -14,10 +14,10 @@ module Foobara
14
14
  if detached_to_primary_key?
15
15
  object.primary_key
16
16
  else
17
- object.attributes
17
+ object.attributes_with_delegates
18
18
  end
19
19
  when Model
20
- object.attributes
20
+ object.attributes_with_delegates
21
21
  when ::Array
22
22
  object.map { |element| serialize(element) }
23
23
  when ::Hash
@@ -3,12 +3,12 @@ module Foobara
3
3
  # TODO: move this to command connectors project
4
4
  class TransformedCommand
5
5
  class << self
6
+ # TODO: handle errors_transformers!
7
+ attr_writer :result_transformers, :inputs_transformers
6
8
  attr_accessor :command_class,
7
9
  :command_name,
8
10
  :full_command_name,
9
11
  :capture_unknown_error,
10
- :inputs_transformers,
11
- :result_transformers,
12
12
  :errors_transformers,
13
13
  :pre_commit_transformers,
14
14
  # TODO: get at least these serializers out of here...
@@ -20,7 +20,9 @@ module Foobara
20
20
  :request_mutators,
21
21
  :allowed_rule,
22
22
  :requires_authentication,
23
- :authenticator
23
+ :authenticator,
24
+ :subclassed_in_namespace,
25
+ :suffix
24
26
 
25
27
  def subclass(
26
28
  command_class,
@@ -40,21 +42,6 @@ module Foobara
40
42
  suffix: nil,
41
43
  capture_unknown_error: false
42
44
  )
43
- result_type = command_class.result_type
44
-
45
- if result_type&.has_sensitive_types?
46
- remover_class = Foobara::TypeDeclarations.sensitive_value_remover_class_for_type(result_type)
47
-
48
- remover = Namespace.use scoped_namespace do
49
- transformed_result_type = result_type_from_transformers(result_type, result_transformers)
50
- remover_class.new(from: transformed_result_type).tap do |r|
51
- r.scoped_path = ["SensitiveValueRemover", *transformed_result_type.scoped_full_path]
52
- end
53
- end
54
-
55
- result_transformers = [*result_transformers, remover]
56
- end
57
-
58
45
  Class.new(self).tap do |klass|
59
46
  klass.command_class = command_class
60
47
  klass.command_name = command_name
@@ -70,6 +57,8 @@ module Foobara
70
57
  klass.allowed_rule = allowed_rule
71
58
  klass.requires_authentication = requires_authentication
72
59
  klass.authenticator = authenticator
60
+ klass.subclassed_in_namespace = scoped_namespace
61
+ klass.suffix = suffix
73
62
  end
74
63
  end
75
64
 
@@ -78,51 +67,125 @@ module Foobara
78
67
  :organization,
79
68
  to: :command_class
80
69
 
81
- def inputs_type
82
- return @inputs_type if defined?(@inputs_type)
70
+ def result_transformers
71
+ return @result_transformers if @considered_sensitive_value_remover
83
72
 
84
- @inputs_type = if inputs_transformer
85
- if inputs_transformer.is_a?(Value::Processor::Pipeline)
86
- inputs_transformer.processors.each do |transformer|
87
- if transformer.is_a?(TypeDeclarations::TypedTransformer)
88
- from_type = transformer.from_type
89
- if from_type
90
- @inputs_type = from_type
91
- return from_type
92
- end
93
- end
94
- end
95
-
96
- command_class.inputs_type
97
- else
98
- if inputs_transformer.is_a?(TypeDeclarations::TypedTransformer)
99
- inputs_transformer.from_type
100
- end || command_class.inputs_type
101
- end
102
- else
103
- command_class.inputs_type
104
- end
73
+ @considered_sensitive_value_remover = true
74
+
75
+ result_type = command_class.result_type
76
+
77
+ result_transformers.reverse.each do |transformer|
78
+ if transformer.is_a?(TypeDeclarations::TypedTransformer) ||
79
+ (transformer.is_a?(Class) && transformer < TypeDeclarations::TypedTransformer)
80
+ new_type = transformer.to_type
81
+ if new_type
82
+ result_type = new_type
83
+ break
84
+ end
85
+ end
86
+ end
87
+
88
+ if result_type&.has_sensitive_types?
89
+ remover_class = Foobara::TypeDeclarations.sensitive_value_remover_class_for_type(result_type)
90
+
91
+ remover = Namespace.use subclassed_in_namespace do
92
+ path = if result_type.scoped_path_set?
93
+ result_type.scoped_full_path
94
+ else
95
+ [*command_class.scoped_path, *suffix, "Result"]
96
+ end
97
+
98
+ remover_class.new(from: result_type).tap do |r|
99
+ r.scoped_path = ["SensitiveValueRemover", *path]
100
+ end
101
+ end
102
+
103
+ @result_transformers = [*@result_transformers, remover]
104
+ end
105
+
106
+ @result_transformers
107
+ end
108
+
109
+ def inputs_transformers
110
+ return @inputs_transformers if @considered_inputs_sensitive_value_remover
111
+
112
+ @considered_inputs_sensitive_value_remover = true
113
+
114
+ inputs_type = command_class.inputs_type
115
+
116
+ @inputs_transformers = transformers_to_processors(@inputs_transformers, inputs_type, direction: :to)
117
+
118
+ @inputs_transformers.each do |transformer|
119
+ if transformer.is_a?(TypeDeclarations::TypedTransformer)
120
+ new_type = transformer.from_type
121
+ if new_type
122
+ inputs_type = new_type
123
+ break
124
+ end
125
+ end
126
+ end
127
+
128
+ @inputs_transformers
129
+ end
130
+
131
+ def inputs_type_for_manifest
132
+ return @inputs_type_for_manifest if defined?(@inputs_type_for_manifest)
133
+
134
+ @inputs_type_for_manifest = if inputs_type&.has_sensitive_types?
135
+ remover_class = Foobara::TypeDeclarations.sensitive_value_remover_class_for_type(
136
+ inputs_type
137
+ )
138
+
139
+ Namespace.use subclassed_in_namespace do
140
+ remover_class.new(to: inputs_type).from_type
141
+ end
142
+ else
143
+ inputs_type
144
+ end
105
145
  end
106
146
 
107
- def result_type_from_transformers(result_type, transformers)
108
- transformers.reverse.each do |transformer|
109
- if transformer.is_a?(Class) && transformer < TypeDeclarations::TypedTransformer
147
+ def inputs_type_from_transformers
148
+ return @inputs_type_from_transformers if defined?(@inputs_type_from_transformers)
149
+
150
+ @inputs_type_from_transformers = if inputs_transformer
151
+ if inputs_transformer.is_a?(Value::Processor::Pipeline)
152
+ inputs_transformer.processors.each do |transformer|
153
+ if transformer.is_a?(TypeDeclarations::TypedTransformer)
154
+ from_type = transformer.from_type
155
+ if from_type
156
+ @inputs_type_from_transformers = from_type
157
+ return from_type
158
+ end
159
+ end
160
+ end
161
+
162
+ command_class.inputs_type
163
+ else
164
+ if inputs_transformer.is_a?(TypeDeclarations::TypedTransformer)
165
+ inputs_transformer.from_type
166
+ end || command_class.inputs_type
167
+ end
168
+ else
169
+ command_class.inputs_type
170
+ end
171
+ end
172
+
173
+ def result_type_from_transformers
174
+ result_transformers.reverse.each do |transformer|
175
+ if transformer.is_a?(TypeDeclarations::TypedTransformer) ||
176
+ (transformer.is_a?(Class) && transformer < TypeDeclarations::TypedTransformer)
110
177
  new_type = transformer.to_type
111
178
  return new_type if new_type
112
179
  end
113
180
  end
114
181
 
115
- result_type
182
+ command_class.result_type
116
183
  end
117
184
 
118
185
  def result_type
119
- result_type_from_transformers(command_class.result_type, result_transformers)
120
- end
186
+ return @result_type if defined?(@result_type)
121
187
 
122
- def result_type_for_manifest
123
- return @result_type_for_manifest if defined?(@result_type_for_manifest)
124
-
125
- mutated_result_type = result_type
188
+ mutated_result_type = result_type_from_transformers
126
189
 
127
190
  mutators = if response_mutators.size == 1
128
191
  [response_mutator]
@@ -134,25 +197,25 @@ module Foobara
134
197
  mutated_result_type = mutator.result_type_from(mutated_result_type)
135
198
  end
136
199
 
137
- @result_type_for_manifest = mutated_result_type
200
+ @result_type = mutated_result_type
138
201
  end
139
202
 
140
- def inputs_type_for_manifest
141
- return @inputs_type_for_manifest if defined?(@inputs_type_for_manifest)
203
+ def inputs_type
204
+ return @inputs_type if defined?(@inputs_type)
142
205
 
143
- mutated_inputs_type = inputs_type
206
+ mutated_inputs_type = inputs_type_from_transformers
144
207
 
145
208
  mutators = if request_mutators.size == 1
146
209
  [request_mutator]
147
210
  else
148
- request_mutator&.processors&.reverse
211
+ request_mutator&.processors
149
212
  end
150
213
 
151
214
  mutators&.each do |mutator|
152
215
  mutated_inputs_type = mutator.inputs_type_from(mutated_inputs_type)
153
216
  end
154
217
 
155
- @inputs_type_for_manifest = mutated_inputs_type
218
+ @inputs_type = mutated_inputs_type
156
219
  end
157
220
 
158
221
  def error_context_type_map
@@ -204,12 +267,12 @@ module Foobara
204
267
 
205
268
  def inputs_types_depended_on
206
269
  TypeDeclarations.with_manifest_context(remove_sensitive: false) do
207
- inputs_type_for_manifest&.types_depended_on || []
270
+ inputs_type&.types_depended_on || []
208
271
  end
209
272
  end
210
273
 
211
274
  def result_types_depended_on
212
- type_proc = -> { result_type_for_manifest&.types_depended_on || [] }
275
+ type_proc = -> { result_type&.types_depended_on || [] }
213
276
 
214
277
  if TypeDeclarations.manifest_context_set?(:remove_sensitive)
215
278
  type_proc.call
@@ -224,21 +287,21 @@ module Foobara
224
287
  # TODO: memoize this
225
288
  # TODO: this should not delegate to command since transformers are in play
226
289
 
227
- type = inputs_type
290
+ types_proc = proc do
291
+ type = inputs_type_for_manifest
292
+
293
+ if type
294
+ if type.registered?
295
+ # TODO: if we ever change from attributes-only inputs type
296
+ # then this will be handy
297
+ # :nocov:
298
+ types |= [type]
299
+ # :nocov:
300
+ end
228
301
 
229
- if type
230
- if type.registered?
231
- # TODO: if we ever change from attributes-only inputs type
232
- # then this will be handy
233
- # :nocov:
234
- types |= [type]
235
- # :nocov:
302
+ types |= type.types_depended_on
236
303
  end
237
304
 
238
- types |= type.types_depended_on
239
- end
240
-
241
- types_proc = proc do
242
305
  type = result_type
243
306
 
244
307
  if type
@@ -312,17 +375,19 @@ module Foobara
312
375
  result_types_depended_on = self.result_types_depended_on.map(&:foobara_manifest_reference)
313
376
  result_types_depended_on = result_types_depended_on.sort
314
377
 
315
- # TODO: Do NOT defer to command_class.foobara_manifest because it might process types that
316
- # might not have an exposed command and might not need one due to transformers/mutators/remove_sensitive flag
317
- command_class.foobara_manifest.merge(
378
+ bit_bucket = Set.new
379
+ manifest = TypeDeclarations.with_manifest_context(to_include: bit_bucket) do
380
+ command_class.foobara_manifest
381
+ end
382
+
383
+ # TODO: handle errors_types_depended_on!
384
+ manifest.merge(
318
385
  Util.remove_blank(
319
386
  inputs_types_depended_on:,
320
387
  result_types_depended_on:,
321
388
  types_depended_on: types,
322
- inputs_type: TypeDeclarations.with_manifest_context(remove_sensitive: false) do
323
- inputs_type_for_manifest&.reference_or_declaration_data
324
- end,
325
- result_type: result_type_for_manifest&.reference_or_declaration_data,
389
+ inputs_type: inputs_type_for_manifest&.reference_or_declaration_data,
390
+ result_type: result_type&.reference_or_declaration_data,
326
391
  possible_errors: possible_errors_manifest,
327
392
  capture_unknown_error:,
328
393
  inputs_transformers:,
@@ -397,13 +462,16 @@ module Foobara
397
462
  def response_mutator
398
463
  return @response_mutator if defined?(@response_mutator)
399
464
 
465
+ # A hack: this will give the SensitiveValueRemover a chance to be injected
466
+ result_transformers
467
+
400
468
  if response_mutators.empty?
401
469
  @response_mutator = nil
402
470
  return
403
471
  end
404
472
 
405
473
  @response_mutator = begin
406
- transformers = transformers_to_processors(response_mutators, result_type, direction: :from)
474
+ transformers = transformers_to_processors(response_mutators, result_type_from_transformers, direction: :from)
407
475
 
408
476
  if transformers.size == 1
409
477
  transformers.first
@@ -416,13 +484,16 @@ module Foobara
416
484
  def request_mutator
417
485
  return @request_mutator if defined?(@request_mutator)
418
486
 
487
+ # HACK: to give SensitiveValueRemover a chance to be injected
488
+ inputs_transformer
489
+
419
490
  if request_mutators.empty?
420
491
  @request_mutator = nil
421
492
  return
422
493
  end
423
494
 
424
495
  @request_mutator = begin
425
- transformers = transformers_to_processors(request_mutators, result_type, direction: :to)
496
+ transformers = transformers_to_processors(request_mutators, inputs_type_from_transformers, direction: :to)
426
497
 
427
498
  if transformers.size == 1
428
499
  transformers.first
@@ -16,6 +16,8 @@ module Foobara
16
16
  end
17
17
 
18
18
  def hash
19
+ # TODO: what about when it originally did not have a primary key but now does? such as
20
+ # after a transaction commit of a freshly created record?
19
21
  (primary_key || object_id).hash
20
22
  end
21
23
  end
@@ -9,13 +9,30 @@ module Foobara
9
9
  def transform(strict_type_declaration)
10
10
  old_attributes_declaration = strict_type_declaration[:attributes_declaration]
11
11
 
12
- new_attributes_declaration = remove_sensitive_types(old_attributes_declaration)
12
+ new_attributes_declaration = old_attributes_declaration
13
13
 
14
- if new_attributes_declaration == old_attributes_declaration
15
- strict_type_declaration
16
- else
17
- strict_type_declaration.merge(attributes_declaration: new_attributes_declaration)
14
+ if strict_type_declaration.key?(:private)
15
+ new_attributes_declaration = TypeDeclarations::Attributes.reject(
16
+ old_attributes_declaration,
17
+ strict_type_declaration[:private]
18
+ )
18
19
  end
20
+
21
+ new_attributes_declaration = remove_sensitive_types(new_attributes_declaration)
22
+
23
+ if new_attributes_declaration != old_attributes_declaration
24
+ strict_type_declaration = strict_type_declaration.merge(attributes_declaration: new_attributes_declaration)
25
+ end
26
+
27
+ if strict_type_declaration.key?(:delegates)
28
+ strict_type_declaration = strict_type_declaration.except(:delegates)
29
+ end
30
+
31
+ if strict_type_declaration.key?(:private)
32
+ strict_type_declaration = strict_type_declaration.except(:private)
33
+ end
34
+
35
+ strict_type_declaration
19
36
  end
20
37
  end
21
38
  end
@@ -5,7 +5,11 @@ module Foobara
5
5
  def transform(record)
6
6
  attributes_type = from_type.element_types
7
7
 
8
- sanitized_attributes, _changed = sanitize_value(attributes_type, record.attributes)
8
+ sanitized_attributes, _changed = sanitize_value(attributes_type, record.attributes_with_delegates)
9
+
10
+ if from_type.declaration_data.key?(:private)
11
+ sanitized_attributes = sanitized_attributes.except(*from_type.declaration_data[:private])
12
+ end
9
13
 
10
14
  Namespace.use(to_type.created_in_namespace) do
11
15
  to_type.target_class.send(build_method, sanitized_attributes)
@@ -94,6 +94,12 @@ module Foobara
94
94
  scoped.scoped_namespace = nil
95
95
  end
96
96
 
97
+ def foobara_unregister_all
98
+ foobara_registry.each_scoped do |child|
99
+ foobara_unregister(child)
100
+ end
101
+ end
102
+
97
103
  def foobara_lookup(
98
104
  path,
99
105
  filter: nil,
@@ -19,7 +19,14 @@ module Foobara
19
19
  subclass.extend ::Foobara::Scoped
20
20
 
21
21
  NamespaceHelpers.foobara_autoset_namespace(subclass, default_namespace: scoped_default_namespace)
22
- NamespaceHelpers.foobara_autoset_scoped_path(subclass)
22
+
23
+ if !subclass.respond_to?(:will_set_scoped_path?) || !subclass.will_set_scoped_path?
24
+ NamespaceHelpers.foobara_autoset_scoped_path(
25
+ subclass,
26
+ set_namespace: true,
27
+ namespace_default: scoped_default_namespace
28
+ )
29
+ end
23
30
 
24
31
  subclass.extend ::Foobara::Namespace::IsNamespace
25
32
  end
@@ -35,6 +42,8 @@ module Foobara
35
42
 
36
43
  subclass.extend ::Foobara::Scoped
37
44
 
45
+ return if subclass.respond_to?(:will_set_scoped_path?) && subclass.will_set_scoped_path?
46
+
38
47
  NamespaceHelpers.foobara_autoset_namespace(subclass, default_namespace: scoped_default_namespace)
39
48
  NamespaceHelpers.foobara_autoset_scoped_path(subclass)
40
49
 
@@ -161,7 +170,12 @@ module Foobara
161
170
  mod.scoped_namespace = default_namespace if default_namespace
162
171
  end
163
172
 
164
- def foobara_autoset_scoped_path(mod, make_top_level: false)
173
+ def anon_sequence(class_name)
174
+ @anon_sequences ||= {}
175
+ @anon_sequences.key?(class_name) ? @anon_sequences[class_name] += 1 : @anon_sequences[class_name] = 1
176
+ end
177
+
178
+ def foobara_autoset_scoped_path(mod, make_top_level: false, set_namespace: false, namespace_default: nil)
165
179
  return if mod.scoped_path_set?
166
180
 
167
181
  mod_name = mod.name
@@ -174,7 +188,13 @@ module Foobara
174
188
  super_name = parent.scoped_path_set? ? parent.scoped_full_name : parent.name
175
189
  end until super_name
176
190
 
177
- mod_name = [super_name, mod.object_id.to_s(16)].join("::")
191
+ short_name = if mod.respond_to?(:symbol) && mod.symbol
192
+ mod.symbol.to_s
193
+ else
194
+ "Anon#{NamespaceHelpers.anon_sequence(super_name)}"
195
+ end
196
+
197
+ mod_name = [super_name, short_name].join("::")
178
198
  end
179
199
 
180
200
  scoped_path = mod_name.split("::")
@@ -183,6 +203,8 @@ module Foobara
183
203
 
184
204
  next_mod = Object
185
205
 
206
+ parent = namespace_default
207
+
186
208
  while next_mod
187
209
  path_part = scoped_path.shift
188
210
 
@@ -192,6 +214,7 @@ module Foobara
192
214
 
193
215
  if next_mod.is_a?(IsNamespace) && next_mod != mod && !make_top_level
194
216
  adjusted_scoped_path = []
217
+ parent = next_mod
195
218
  next
196
219
  end
197
220
 
@@ -201,6 +224,10 @@ module Foobara
201
224
  mod.scoped_path_autoset = true
202
225
  mod.scoped_path = adjusted_scoped_path
203
226
 
227
+ if set_namespace
228
+ mod.scoped_namespace = parent
229
+ end
230
+
204
231
  if mod.is_a?(IsNamespace)
205
232
  update_children_with_new_parent(mod)
206
233
  end
@@ -114,6 +114,7 @@ module Foobara
114
114
 
115
115
  Entity.after_initialized_created do |record:, **|
116
116
  transaction = Persistence.current_transaction(record)
117
+
117
118
  unless transaction
118
119
  raise NoCurrentTransactionError,
119
120
  "Cannot initialize #{record} because there's no current transaction"
@@ -131,6 +132,7 @@ module Foobara
131
132
 
132
133
  Entity.after_initialized_thunk do |record:, **|
133
134
  transaction = Persistence.current_transaction(record)
135
+
134
136
  unless transaction
135
137
  # :nocov:
136
138
  raise NoCurrentTransactionError,
@@ -21,9 +21,8 @@ module Foobara
21
21
  attributes = normalize_attributes(attributes)
22
22
  primary_key = primary_key_for_attributes(attributes)
23
23
 
24
- if tracked_records.include_key?(primary_key)
25
- record = tracked_records.find_by_key(primary_key)
26
-
24
+ record = tracked_records.find_by_key(primary_key)
25
+ if record
27
26
  next if record.hard_deleted?
28
27
  next if created?(record)
29
28
 
@@ -34,7 +34,7 @@ module Foobara
34
34
  end
35
35
 
36
36
  def created(record)
37
- tracked_records << record
37
+ tracked(record)
38
38
  mark_created(record)
39
39
  end
40
40
 
@@ -64,7 +64,7 @@ module Foobara
64
64
  end
65
65
 
66
66
  def updated(record)
67
- tracked_records << record
67
+ tracked(record)
68
68
 
69
69
  # TODO: is this check redundant? Maybe have the entity explode directly instead?
70
70
  if hard_deleted?(record)
@@ -65,16 +65,29 @@ module Foobara
65
65
  end
66
66
 
67
67
  def load(entity_or_record_id)
68
- if entity_or_record_id.is_a?(Entity)
69
- if entity_or_record_id.loaded?
70
- # :nocov:
71
- raise "#{entity_or_record_id} is already loaded!"
72
- # :nocov:
73
- end
74
-
75
- entity = tracked_records[entity_or_record_id]
68
+ if entity_or_record_id.nil?
69
+ # :nocov:
70
+ raise "Expected a record or record primary key but received nil"
71
+ # :nocov:
72
+ end
76
73
 
77
- if entity && !entity.equal?(entity_or_record_id)
74
+ if entity_or_record_id.is_a?(Entity)
75
+ entity = if entity_or_record_id.persisted?
76
+ unless entity_or_record_id.primary_key
77
+ # :nocov:
78
+ raise "Did not expect a record to be persisted but have no primary key"
79
+ # :nocov:
80
+ end
81
+
82
+ tracked_records.find_by_key(entity_or_record_id.primary_key)
83
+ else
84
+ # :nocov:
85
+ raise "Cannot load an unpersisted record!"
86
+ # :nocov:
87
+ end
88
+
89
+ if entity &&
90
+ (!entity.equal?(entity_or_record_id) || entity.object_id != entity_or_record_id.object_id)
78
91
  # :nocov:
79
92
  raise "This transaction is already tracking a different entity with the same primary key." \
80
93
  "Try passing in the primary key instead of constructing an unloaded entity to pass in."
@@ -151,7 +164,19 @@ module Foobara
151
164
  next
152
165
  end
153
166
 
154
- entity = tracked_records[entity_or_record_id]
167
+ entity = if entity_or_record_id.persisted?
168
+ unless entity_or_record_id.primary_key
169
+ # :nocov:
170
+ raise "Did not expect a record to be persisted but have no primary key"
171
+ # :nocov:
172
+ end
173
+
174
+ tracked_records.find_by_key(entity_or_record_id.primary_key)
175
+ else
176
+ # :nocov:
177
+ raise "Cannot load an unpersisted record!"
178
+ # :nocov:
179
+ end
155
180
 
156
181
  if entity && !entity.equal?(entity_or_record_id)
157
182
  # :nocov:
@@ -440,7 +465,7 @@ module Foobara
440
465
  end
441
466
 
442
467
  def track_loaded(entity)
443
- tracked_records << entity
468
+ tracked(entity)
444
469
  end
445
470
 
446
471
  def hard_delete_all!
@@ -527,7 +552,7 @@ module Foobara
527
552
 
528
553
  # we need to update finding the tracked object by key and removing/reading it seems to be the simplest
529
554
  # way to accomplish that at the moment
530
- tracked_records << record
555
+ tracked(record)
531
556
 
532
557
  record.is_persisted = record.is_loaded = true
533
558
  record.is_created = false
@@ -547,7 +572,7 @@ module Foobara
547
572
 
548
573
  # we need to update finding the tracked object by key and removing/reading it seems to be the simplest
549
574
  # way to accomplish that at the moment
550
- tracked_records << record
575
+ tracked(record)
551
576
 
552
577
  record.is_persisted = record.is_loaded = true
553
578
  record.is_created = false
@@ -73,6 +73,9 @@ module Foobara
73
73
  register_sensitive_value_remover(attributes_handler, SensitiveValueRemovers::Attributes)
74
74
  register_sensitive_type_remover(SensitiveTypeRemovers::Array.new(array_handler))
75
75
  register_sensitive_value_remover(array_handler, SensitiveValueRemovers::Array)
76
+
77
+ Foobara::AttributesTransformers::Reject.foobara_unregister_all
78
+ Foobara::AttributesTransformers::Only.foobara_unregister_all
76
79
  end
77
80
 
78
81
  def install!
@@ -5,6 +5,10 @@ module Foobara
5
5
  transformer_class = Class.new(Only)
6
6
  transformer_class.only_attributes = attribute_names
7
7
 
8
+ Namespace::NamespaceHelpers.foobara_autoset_scoped_path(transformer_class, set_namespace: true)
9
+ transformer_class.foobara_parent_namespace = transformer_class.scoped_namespace
10
+ transformer_class.scoped_namespace.foobara_register(transformer_class)
11
+
8
12
  transformer_class
9
13
  end
10
14
  end
@@ -12,6 +16,14 @@ module Foobara
12
16
  class Only < AttributesTransformers
13
17
  class << self
14
18
  attr_accessor :only_attributes
19
+
20
+ def symbol
21
+ only_attributes&.sort&.join("_")&.to_sym
22
+ end
23
+
24
+ def will_set_scoped_path?
25
+ true
26
+ end
15
27
  end
16
28
 
17
29
  def to_type_declaration
@@ -5,6 +5,10 @@ module Foobara
5
5
  transformer_class = Class.new(Reject)
6
6
  transformer_class.reject_attributes = attribute_names
7
7
 
8
+ Namespace::NamespaceHelpers.foobara_autoset_scoped_path(transformer_class, set_namespace: true)
9
+ transformer_class.foobara_parent_namespace = transformer_class.scoped_namespace
10
+ transformer_class.scoped_namespace.foobara_register(transformer_class)
11
+
8
12
  transformer_class
9
13
  end
10
14
  end
@@ -12,6 +16,14 @@ module Foobara
12
16
  class Reject < AttributesTransformers
13
17
  class << self
14
18
  attr_accessor :reject_attributes
19
+
20
+ def symbol
21
+ reject_attributes&.sort&.join("_")&.to_sym
22
+ end
23
+
24
+ def will_set_scoped_path?
25
+ true
26
+ end
15
27
  end
16
28
 
17
29
  def to_type_declaration
@@ -0,0 +1,11 @@
1
+ module Foobara
2
+ class AttributesTransformers < TypeDeclarations::TypedTransformer
3
+ def initialize(...)
4
+ super
5
+
6
+ unless scoped_path_set?
7
+ self.scoped_path = self.class.scoped_path
8
+ end
9
+ end
10
+ end
11
+ end
@@ -5,20 +5,30 @@ module Foobara
5
5
  class RemoveSensitiveValuesTransformer < TypedTransformer
6
6
  def from(...)
7
7
  super.tap do
8
- associations = Foobara::DetachedEntity.construct_deep_associations(from_type)
8
+ create_all_association_types_in_current_namespace(from_type)
9
+ end
10
+ end
11
+
12
+ def to(...)
13
+ super.tap do
14
+ create_all_association_types_in_current_namespace(to_type)
15
+ end
16
+ end
9
17
 
10
- associations&.values&.reverse&.each do |entity_type|
11
- next if entity_type.sensitive?
12
- next unless entity_type.has_sensitive_types?
18
+ def create_all_association_types_in_current_namespace(type)
19
+ associations = Foobara::DetachedEntity.construct_deep_associations(type)
13
20
 
14
- declaration = entity_type.declaration_data
15
- sanitized_type_declaration = TypeDeclarations.remove_sensitive_types(declaration)
21
+ associations&.values&.reverse&.each do |entity_type|
22
+ next if entity_type.sensitive?
23
+ next unless entity_type.has_sensitive_types?
16
24
 
17
- # We want to make sure that any types that change due to having sensitive types
18
- # has a corresponding registered type in the command registry domain if needed
19
- # TODO: this all feels so messy and brittle.
20
- Domain.current.foobara_type_from_declaration(sanitized_type_declaration)
21
- end
25
+ declaration = entity_type.declaration_data
26
+ sanitized_type_declaration = TypeDeclarations.remove_sensitive_types(declaration)
27
+
28
+ # We want to make sure that any types that change due to having sensitive types
29
+ # has a corresponding registered type in the command registry domain if needed
30
+ # TODO: this all feels so messy and brittle.
31
+ Domain.current.foobara_type_from_declaration(sanitized_type_declaration)
22
32
  end
23
33
  end
24
34
 
@@ -26,6 +36,10 @@ module Foobara
26
36
  TypeDeclarations.remove_sensitive_types(from_type.declaration_data)
27
37
  end
28
38
 
39
+ def from_type_declaration
40
+ TypeDeclarations.remove_sensitive_types(to_type.declaration_data)
41
+ end
42
+
29
43
  def transform(_value)
30
44
  # :nocov:
31
45
  raise "subclass responsibility"
@@ -73,7 +73,7 @@ module Foobara
73
73
  self.to to
74
74
  end
75
75
 
76
- # we want to force these to be created now in the current name space if they are declarations
76
+ # we want to force these to be created now in the current namespace if they are declarations
77
77
  from_type
78
78
  to_type
79
79
  end
@@ -324,6 +324,12 @@ module Foobara
324
324
  def foobara_manifest
325
325
  to_include = TypeDeclarations.foobara_manifest_context_to_include
326
326
 
327
+ unless scoped_path_set?
328
+ if self.class.scoped_path_set?
329
+ self.scoped_path = self.class.scoped_path
330
+ end
331
+ end
332
+
327
333
  possible_errors = self.possible_errors.map do |possible_error|
328
334
  [possible_error.key.to_s, possible_error.foobara_manifest]
329
335
  end
@@ -1,52 +1,98 @@
1
+ require "monitor"
2
+
1
3
  module Foobara
4
+ # TODO: a possible optimization: have a certain number of records before the Weakref approach kicks in
5
+ # that way we don't just immediately clear out useful information without any actual memory burden
2
6
  class WeakObjectSet
7
+ class InvalidWtf < StandardError; end
8
+
3
9
  class GarbageCleaner
4
- def initialize(objects, key_to_object_id = nil, object_id_to_key = nil)
5
- @objects = objects
6
- @key_to_object_id = key_to_object_id
7
- @object_id_to_key = object_id_to_key
8
- end
10
+ attr_accessor :weak_object_set, :deactivated, :queue, :cleanup_thread
9
11
 
10
- def cleanup_proc
11
- @cleanup_proc ||= ->(object_id) {
12
- unless @deactivated
13
- @objects.delete(object_id)
12
+ def initialize(weak_object_set, queue)
13
+ self.queue = queue
14
+ self.weak_object_set = weak_object_set
14
15
 
15
- key = @object_id_to_key&.delete(object_id)
16
+ start_cleanup_thread
17
+ end
16
18
 
17
- if key
18
- @key_to_object_id.delete(key)
19
+ def cleanup_proc
20
+ @cleanup_proc ||= begin
21
+ queue = self.queue
22
+
23
+ ->(object_id) do
24
+ unless deactivated?
25
+ begin
26
+ queue.push(object_id)
27
+ rescue ClosedQueueError
28
+ # :nocov:
29
+ deactivate
30
+ # :nocov:
31
+ end
19
32
  end
20
33
  end
21
- }
34
+ end
22
35
  end
23
36
 
24
- def track(object)
25
- if @deactivated
26
- # :nocov:
27
- raise "Cannot track anymore objects since we have been deactivated"
28
- # :nocov:
37
+ def start_cleanup_thread
38
+ self.cleanup_thread = Thread.new do
39
+ loop do
40
+ object_id = queue.pop
41
+ if object_id
42
+ weak_object_set.delete(object_id)
43
+ elsif queue.closed?
44
+ self.queue = nil
45
+ break
46
+ else
47
+ # :nocov:
48
+ raise "Unexpected nil value in the queue"
49
+ # :nocov:
50
+ end
51
+ end
29
52
  end
53
+ end
30
54
 
55
+ def track(object)
31
56
  ObjectSpace.define_finalizer(object, cleanup_proc)
32
57
  end
33
58
 
34
59
  def deactivate
35
- @deactivated = true
60
+ self.deactivated = true
61
+ queue.close
62
+ cleanup_thread.join # just doing this for test suite/simplecov
63
+ end
64
+
65
+ def deactivated?
66
+ deactivated
36
67
  end
37
68
  end
38
69
 
39
70
  include Enumerable
40
71
 
72
+ attr_accessor :monitor, :key_method, :key_to_object_id, :object_id_to_key, :objects
73
+ attr_writer :garbage_cleaner
74
+
41
75
  def initialize(key_method = nil)
42
- @key_method = key_method
76
+ self.key_method = key_method
77
+ self.monitor = Monitor.new
78
+ clear
43
79
  end
44
80
 
45
81
  def [](object_or_object_id)
46
- ref = ref_for(object_or_object_id)
82
+ monitor.synchronize do
83
+ ref = ref_for(object_or_object_id)
47
84
 
48
- if ref&.weakref_alive?
49
- ref.__getobj__
85
+ object = begin
86
+ ref&.__getobj__
87
+ rescue WeakRef::RefError
88
+ # :nocov:
89
+ nil
90
+ # :nocov:
91
+ end
92
+
93
+ if ref&.weakref_alive?
94
+ object
95
+ end
50
96
  end
51
97
  end
52
98
 
@@ -61,121 +107,146 @@ module Foobara
61
107
  end
62
108
 
63
109
  def each
64
- objects.each_value do |ref|
65
- if ref.weakref_alive?
66
- yield ref.__getobj__
110
+ monitor.synchronize do
111
+ objects.each_value do |ref|
112
+ object = begin
113
+ ref.__getobj__
114
+ rescue WeakRef::RefError
115
+ nil
116
+ end
117
+
118
+ if ref.weakref_alive?
119
+ yield object
120
+ end
67
121
  end
68
122
  end
69
123
  end
70
124
 
71
- def objects
72
- @objects ||= {}
73
- end
74
-
75
125
  def size
76
126
  count
77
127
  end
78
128
 
79
129
  def empty?
80
- objects.empty? || objects.values.none?(&:weakref_alive?)
130
+ monitor.synchronize do
131
+ objects.empty? || objects.values.none?(&:weakref_alive?)
132
+ end
81
133
  end
82
134
 
83
- def key_to_object_id
84
- @key_to_object_id ||= {}
85
- end
135
+ def garbage_cleaner
136
+ @garbage_cleaner ||= begin
137
+ queue = Queue.new
86
138
 
87
- def object_id_to_key
88
- @object_id_to_key ||= {}
89
- end
139
+ gc = GarbageCleaner.new(self, queue)
90
140
 
91
- def garbage_cleaner
92
- @garbage_cleaner ||= if @key_method
93
- GarbageCleaner.new(objects, key_to_object_id, object_id_to_key)
94
- else
95
- GarbageCleaner.new(objects)
96
- end
141
+ ObjectSpace.define_finalizer gc do
142
+ # :nocov:
143
+ queue.close
144
+ # :nocov:
145
+ end
146
+
147
+ gc
148
+ end
97
149
  end
98
150
 
99
151
  def <<(object)
100
152
  object_id = object.object_id
101
153
 
102
- if include?(object)
103
- if @key_method
104
- key = object.send(@key_method)
105
- old_key = object_id_to_key[object_id]
154
+ monitor.synchronize do
155
+ existing_object = self[object_id]
156
+
157
+ if existing_object
158
+ if key_method
159
+ key = object.send(key_method)
160
+ old_key = object_id_to_key[object_id]
106
161
 
107
- if key != old_key
108
- key_to_object_id.delete(old_key)
162
+ if key != old_key
163
+ key_to_object_id.delete(old_key)
164
+
165
+ if key
166
+ key_to_object_id[key] = object_id
167
+ object_id_to_key[object_id] = key
168
+ else
169
+ object_id_to_key.delete(object_id)
170
+ end
171
+ end
172
+ end
173
+ else
174
+ garbage_cleaner.track(object)
175
+
176
+ if key_method
177
+ key = object.send(key_method)
109
178
 
110
179
  if key
180
+ existing_record_object_id = key_to_object_id[key]
181
+
182
+ if existing_record_object_id
183
+ # Sometimes this path is hit in the test suite and sometimes not, depending on
184
+ # non-deterministic behavior of the garbage collector
185
+ # :nocov:
186
+ delete(existing_record_object_id)
187
+ # :nocov:
188
+ end
189
+
111
190
  key_to_object_id[key] = object_id
112
191
  object_id_to_key[object_id] = key
113
- else
114
- object_id_to_key.delete(object_id)
115
192
  end
116
193
  end
117
- end
118
- else
119
- garbage_cleaner.track(object)
120
-
121
- objects[object_id] = WeakRef.new(object)
122
194
 
123
- if @key_method
124
- key = object.send(@key_method)
195
+ objects[object_id] = WeakRef.new(object)
125
196
 
126
- if key
127
- key_to_object_id[key] = object_id
128
- object_id_to_key[object_id] = key
129
- end
197
+ object
130
198
  end
131
-
132
- object
133
199
  end
134
200
  end
135
201
 
136
- def include?(object_or_object_id)
137
- ref_for(object_or_object_id)&.weakref_alive?
138
- end
202
+ def delete(object_or_object_id)
203
+ object_id = if object_or_object_id.is_a?(::Integer)
204
+ object_or_object_id
205
+ else
206
+ object_or_object_id.object_id
207
+ end
139
208
 
140
- def delete(object)
141
- if @key_method
142
- key = @object_id_to_key&.delete(object.object_id)
209
+ monitor.synchronize do
210
+ if key_method
211
+ key = object_id_to_key.delete(object_id)
143
212
 
144
- if key
145
- @key_to_object_id.delete(key)
213
+ if key
214
+ key_to_object_id.delete(key)
215
+ end
146
216
  end
147
- end
148
-
149
- objects.delete(object)
150
- end
151
217
 
152
- def include_key?(key)
153
- unless @key_method
154
- # :nocov:
155
- raise "Cannot check by key if there was no key_method given."
156
- # :nocov:
218
+ objects.delete(object_id)
157
219
  end
158
-
159
- objects[key_to_object_id[key]]&.weakref_alive?
160
220
  end
161
221
 
162
222
  def find_by_key(key)
163
- unless @key_method
164
- # :nocov:
165
- raise "Cannot find by key if there was no key_method given."
166
- # :nocov:
167
- end
223
+ monitor.synchronize do
224
+ unless key_method
225
+ # :nocov:
226
+ raise "Cannot find by key if there was no key_method given."
227
+ # :nocov:
228
+ end
168
229
 
169
- object = objects[key_to_object_id[key]]
230
+ object_id = key_to_object_id[key]
170
231
 
171
- if object&.weakref_alive?
172
- object.__getobj__
232
+ if object_id
233
+ self[object_id]
234
+ end
173
235
  end
174
236
  end
175
237
 
176
238
  def clear
177
- @garbage_cleaner&.deactivate
178
- @key_to_object_id = @object_id_to_key = @objects = @garbage_cleaner = nil
239
+ monitor.synchronize do
240
+ garbage_cleaner.deactivate
241
+
242
+ self.garbage_cleaner = nil
243
+ self.objects = {}
244
+
245
+ if key_method
246
+ self.key_to_object_id = {}
247
+ self.object_id_to_key = {}
248
+ end
249
+ end
179
250
  end
180
251
  end
181
252
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.118
4
+ version: 0.0.120
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-07 00:00:00.000000000 Z
10
+ date: 2025-05-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -396,6 +396,7 @@ files:
396
396
  - projects/state_machine/src/validations.rb
397
397
  - projects/type_declarations/lib/foobara/type_declarations.rb
398
398
  - projects/type_declarations/src/attributes.rb
399
+ - projects/type_declarations/src/attributes_transformers.rb
399
400
  - projects/type_declarations/src/attributes_transformers/only.rb
400
401
  - projects/type_declarations/src/attributes_transformers/reject.rb
401
402
  - projects/type_declarations/src/caster.rb