foobara 0.1.6 → 0.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3eea1befe835410f5c6ecc372c5db94e3641f90711a773b0a7685a18afe677f
4
- data.tar.gz: 7fe5e33084023a8d475e0fe491520a179d9123b5109c0ebc2ad52ed5643752bd
3
+ metadata.gz: 497323362ef9b9bcf4a7d17cb9400b84701e73dc464f3ff318e6a52b6ee3bbf6
4
+ data.tar.gz: ee7bc81ea47e850b90e569c4295bd914832c3604277790edd6db153e143515a6
5
5
  SHA512:
6
- metadata.gz: 67bbdfab3b295bbe38f39d978f2855a239092f9feba4a417bbb853759dae9527dcbd0ba3a34647925f2b90fc15693c03ddb56a5d5e9247678c22c236deedba2c
7
- data.tar.gz: bc09a9a484f9c1e6487a7ce112e33aabb5132b77071f195ed06cafb79895c070ee895be8e3cdd01abf75c4fc4eb54c51dd23d0bd4b22f7d1e0ef3b584c5874d7
6
+ metadata.gz: acc9a5fed9bdfa6f7c19d74652b6c2bf770a24c17880968dc476289d2231ea51b906c538ee3d10dc65365c74d9d97f9f747ae3c97400789ce30e60babc9ca6b2
7
+ data.tar.gz: 03225ae2b838efc6816fe072b0f94ea6193552bb1253159aaa749ec471c18d4ea821c399cba711f6f6246cf1c38686d398bb4cd5c6385daebf91014a3ab88436
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # [0.1.8] - 2025-08-25
2
+
3
+ - Memoize various parts of Type
4
+ - Eliminate DoesNotNeedCastIf* processors
5
+
6
+ # [0.1.7] - 2025-08-25
7
+
8
+ - Go back to using :detached_entity for entities that have had sensitive types removed to
9
+ avoid casting problems with multiple entities with the same name
10
+ - Remove private attributes as if they were sensitive attributes when exposing models
11
+
1
12
  # [0.1.6] - 2025-08-25
2
13
 
3
14
  - Allow constructing a thunk-like record when removing sensitive values from a thunk
@@ -3,15 +3,15 @@ module Foobara
3
3
  class Block
4
4
  module Concerns
5
5
  module BlockParameterNotAllowed
6
+ class BlockParameterNotAllowedError < StandardError; end
7
+
6
8
  private
7
9
 
8
10
  def validate_original_block!
9
11
  super
10
12
 
11
13
  if takes_block?
12
- # :nocov:
13
- raise ArgumentError, "#{type} callback is not allowed to accept a block"
14
- # :nocov:
14
+ raise BlockParameterNotAllowedError, "#{type} callback is not allowed to accept a block"
15
15
  end
16
16
  end
17
17
  end
@@ -6,6 +6,8 @@ module Foobara
6
6
  include Concern
7
7
 
8
8
  module ClassMethods
9
+ # TODO: consider renaming this to symbol? Could be confused with Foobara::Type concept
10
+ # Returns things like :before, :after, :around, :error to indicate what type of callback it is
9
11
  def type
10
12
  @type ||= Util.non_full_name_underscore(self)&.gsub(/_block$/, "")&.to_sym
11
13
  end
@@ -26,7 +26,9 @@ module Foobara
26
26
  validate_original_block!
27
27
  end
28
28
 
29
- foobara_delegate :type, to: :class
29
+ def type
30
+ self.class.type
31
+ end
30
32
 
31
33
  def call(...)
32
34
  to_proc.call(...)
@@ -48,7 +50,7 @@ module Foobara
48
50
  end
49
51
 
50
52
  def takes_block?
51
- @takes_block ||= original_block.parameters.last&.first&.==(:block)
53
+ @takes_block ||= original_block.parameters.last&.first&.==(:block) || false
52
54
  end
53
55
 
54
56
  def has_one_or_zero_positional_args?
@@ -0,0 +1,26 @@
1
+ require_relative "primary_key"
2
+
3
+ module Foobara
4
+ module BuiltinTypes
5
+ module Entity
6
+ module Casters
7
+ class RecordFromClosedTransaction < PrimaryKey
8
+ def applicable?(value)
9
+ if value.is_a?(entity_class) && value.persisted?
10
+ tx = value.class.entity_base.current_transaction
11
+
12
+ if tx&.currently_open?
13
+ # TODO: might be safer/more performant to store the transaction on the record?
14
+ !value.class.entity_base.current_transaction.tracking?(value)
15
+ end
16
+ end
17
+ end
18
+
19
+ def transform(record)
20
+ super(record.primary_key)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "primary_key"
2
+
3
+ module Foobara
4
+ module BuiltinTypes
5
+ module Entity
6
+ module Casters
7
+ class RecordFromCurrentTransaction < PrimaryKey
8
+ def applicable?(value)
9
+ if value.is_a?(entity_class)
10
+ tx = entity_class.entity_base.current_transaction
11
+ !tx&.currently_open? || tx.tracking?(value)
12
+ end
13
+ end
14
+
15
+ def transform(record)
16
+ record
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -7,12 +7,11 @@ module Foobara
7
7
  super.tap do |outcome|
8
8
  if outcome.success?
9
9
  type = outcome.result
10
+ type.cast_even_if_instance_of_target_type = true
10
11
  entity_class = type.target_class
11
12
 
12
13
  unless entity_class.can_be_created_through_casting?
13
- type.casters = type.casters.reject do |caster|
14
- caster.is_a?(Foobara::BuiltinTypes::Entity::Casters::Hash)
15
- end
14
+ type.remove_caster_instances_of(Foobara::BuiltinTypes::Entity::Casters::Hash)
16
15
  end
17
16
  end
18
17
  end
@@ -7,14 +7,13 @@ module Foobara
7
7
 
8
8
  if strict_type_declaration != new_type_declaration
9
9
  if new_type_declaration[:type] == :entity
10
- if Namespace.current.foobara_root_namespace == Namespace.global.foobara_root_namespace
11
- # Nervous about creating two entities with the same name in the same namespace
12
- # So going to create a detached entity instead
13
- new_type_declaration[:type] = :detached_entity
10
+ # It's important that we don't create another entity with different attributes
11
+ # or various things like crud drivers or type transformers can become confused.
12
+ # So we will create it as a detached_entity instead.
13
+ new_type_declaration[:type] = :detached_entity
14
14
 
15
- if new_type_declaration[:model_base_class] == "Foobara::Entity"
16
- new_type_declaration[:model_base_class] = "Foobara::DetachedEntity"
17
- end
15
+ if new_type_declaration[:model_base_class] == "Foobara::Entity"
16
+ new_type_declaration[:model_base_class] = "Foobara::DetachedEntity"
18
17
  end
19
18
  end
20
19
  end
@@ -8,7 +8,7 @@ module Foobara
8
8
  elsif record.persisted?
9
9
  # We will assume that we do not need to clean up the primary key itself as
10
10
  # we will assume we don't allow sensitive primary keys for now.
11
- thunkish = to_type.target_class.build(record.class.primary_key_attribute => record.primary_key)
11
+ thunkish = to_type.target_class.send(build_method, record.class.primary_key_attribute => record.primary_key)
12
12
  thunkish.skip_validations = true
13
13
  thunkish.mutable = false
14
14
  thunkish
@@ -20,7 +20,14 @@ module Foobara
20
20
  end
21
21
 
22
22
  def build_method
23
- :build
23
+ if to_type.extends?(BuiltinTypes[:entity])
24
+ # TODO: figure out a way to test this path
25
+ # :nocov:
26
+ :build
27
+ # :nocov:
28
+ else
29
+ :new
30
+ end
24
31
  end
25
32
  end
26
33
  end
@@ -15,20 +15,21 @@ module Foobara
15
15
  end
16
16
 
17
17
  attr_accessor :base_type,
18
- :casters,
19
- :transformers,
20
- :validators,
21
- :element_processors,
22
18
  :structure_count,
23
19
  :is_builtin,
24
20
  :name,
25
- :target_classes,
26
21
  :description,
27
22
  :sensitive,
28
- :sensitive_exposed,
29
- :processor_classes_requiring_type
23
+ :sensitive_exposed
30
24
 
31
- attr_reader :type_symbol
25
+ attr_reader :type_symbol,
26
+ :casters,
27
+ :transformers,
28
+ :validators,
29
+ :target_classes,
30
+ :processor_classes_requiring_type,
31
+ :element_processors,
32
+ :cast_even_if_instance_of_target_type
32
33
 
33
34
  attr_writer :element_types,
34
35
  :element_type
@@ -116,6 +117,12 @@ module Foobara
116
117
  def has_sensitive_types?
117
118
  return true if sensitive?
118
119
 
120
+ # TODO: this is a hack... come up with a better/separate way to detect types with private attributes
121
+ if declaration_data.is_a?(::Hash)
122
+ private = declaration_data[:private]
123
+ return true if private.is_a?(::Array) && !private.empty?
124
+ end
125
+
119
126
  if element_type
120
127
  return true if element_type.has_sensitive_types?
121
128
  end
@@ -162,9 +169,34 @@ module Foobara
162
169
  category.delete_if { |p| p.symbol == symbol }
163
170
 
164
171
  category << processor
172
+ clear_caches
173
+ end
174
+ end
175
+
176
+ def clear_caches
177
+ [
178
+ :@value_validator,
179
+ :@processors,
180
+ :@value_caster,
181
+ :@value_transformer,
182
+ :@element_processor,
183
+ :@possible_errors,
184
+ :@processors_without_casters
185
+ ].each do |instance_variable|
186
+ if instance_variable_defined?(instance_variable)
187
+ remove_instance_variable(instance_variable)
188
+ end
165
189
  end
166
190
  end
167
191
 
192
+ def remove_caster_instances_of(klass)
193
+ self.casters = casters.reject do |caster|
194
+ caster.is_a?(klass)
195
+ end
196
+
197
+ clear_caches
198
+ end
199
+
168
200
  def remove_processor_by_symbol(symbol)
169
201
  [
170
202
  casters,
@@ -178,6 +210,7 @@ module Foobara
178
210
  end
179
211
  supported_processor_classes&.each { |processor_hash| processor_hash.delete(symbol) }
180
212
  processor_classes_requiring_type&.delete_if { |p| p.symbol == symbol }
213
+ clear_caches
181
214
  end
182
215
 
183
216
  def each_processor_class_requiring_type(&block)
@@ -212,6 +245,8 @@ module Foobara
212
245
  end
213
246
  end
214
247
  end
248
+
249
+ clear_caches
215
250
  end
216
251
 
217
252
  def target_class
@@ -293,11 +328,52 @@ module Foobara
293
328
  base_type&.extends_type?(type)
294
329
  end
295
330
 
331
+ def processors=(...)
332
+ clear_caches
333
+ super
334
+ end
335
+
296
336
  def type_symbol=(type_symbol)
297
337
  @scoped_path ||= type_symbol.to_s.split("::")
338
+ clear_caches
298
339
  @type_symbol = type_symbol.to_sym
299
340
  end
300
341
 
342
+ def cast_even_if_instance_of_target_type=(flag)
343
+ clear_caches
344
+ @cast_even_if_instance_of_target_type = flag
345
+ end
346
+
347
+ def casters=(processors)
348
+ clear_caches
349
+ @casters = processors
350
+ end
351
+
352
+ def transformers=(processors)
353
+ clear_caches
354
+ @transformers = processors
355
+ end
356
+
357
+ def validators=(processors)
358
+ clear_caches
359
+ @validators = processors
360
+ end
361
+
362
+ def target_classes=(processors)
363
+ clear_caches
364
+ @target_classes = processors
365
+ end
366
+
367
+ def processor_classes_requiring_type=(processors)
368
+ clear_caches
369
+ @processor_classes_requiring_type = processors
370
+ end
371
+
372
+ def element_processors=(processors)
373
+ clear_caches
374
+ @element_processors = processors
375
+ end
376
+
301
377
  def full_type_symbol
302
378
  return @full_type_symbol if defined?(@full_type_symbol)
303
379
 
@@ -307,17 +383,16 @@ module Foobara
307
383
  end
308
384
 
309
385
  def processors
310
- [
386
+ @processors ||= [
311
387
  value_caster,
312
388
  value_transformer,
313
389
  value_validator,
314
390
  element_processor
315
- ].compact
391
+ ].compact.sort_by(&:priority)
316
392
  end
317
393
 
318
394
  def value_caster
319
- # TODO: figure out what would be needed to successfully memoize this
320
- # return @value_caster if defined?(@value_caster)
395
+ return @value_caster if defined?(@value_caster)
321
396
 
322
397
  # We make this exception for :duck because it will match any instance of
323
398
  # Object but AllowNil will match nil which is also an instance of Object.
@@ -329,16 +404,19 @@ module Foobara
329
404
  true
330
405
  end
331
406
 
332
- Value::Processor::Casting.new(
333
- { cast_to: reference_or_declaration_data },
334
- casters:,
335
- target_classes:,
336
- enforce_unique:
337
- )
407
+ Namespace.use created_in_namespace do
408
+ @value_caster = Value::Processor::Casting.new(
409
+ { cast_to: reference_or_declaration_data },
410
+ casters:,
411
+ target_classes:,
412
+ enforce_unique:,
413
+ cast_even_if_instance_of_target_type:
414
+ )
415
+ end
338
416
  end
339
417
 
340
418
  def applicable?(value)
341
- value_caster.can_cast?(value)
419
+ !value_caster.needs_cast?(value) || value_caster.can_cast?(value)
342
420
  end
343
421
 
344
422
  foobara_delegate :needs_cast?, to: :value_caster
@@ -356,24 +434,30 @@ module Foobara
356
434
  # method in the instance of the processor as needed. This means it can't really memoize stuff. Should we create
357
435
  # an instance of something from the instance of the processor and then ask it questions?? TODO: try this
358
436
  def value_transformer
359
- if transformers && !transformers.empty?
360
- Value::Processor::Pipeline.new(processors: transformers)
361
- end
437
+ return @value_transformer if defined?(@value_transformer)
438
+
439
+ @value_transformer = if transformers && !transformers.empty?
440
+ Value::Processor::Pipeline.new(processors: transformers)
441
+ end
362
442
  end
363
443
 
364
444
  # TODO: figure out how to safely memoize stuff so like this for performance reasons
365
445
  # A good way, but potentially a decent amount of work, is to have a class that takes value to its initialize
366
446
  # method.
367
447
  def value_validator
368
- if validators && !validators.empty?
369
- Value::Processor::Pipeline.new(processors: validators)
370
- end
448
+ return @value_validator if defined?(@value_validator)
449
+
450
+ @value_validator = if validators && !validators.empty?
451
+ Value::Processor::Pipeline.new(processors: validators)
452
+ end
371
453
  end
372
454
 
373
455
  def element_processor
374
- if element_processors && !element_processors.empty?
375
- Value::Processor::Pipeline.new(processors: element_processors)
376
- end
456
+ return @element_processor if defined?(@element_processor)
457
+
458
+ @element_processor = if element_processors && !element_processors.empty?
459
+ Value::Processor::Pipeline.new(processors: element_processors)
460
+ end
377
461
  end
378
462
 
379
463
  # TODO: some way of memoizing these values? Would need to introduce a new class that takes the value to its
@@ -37,57 +37,34 @@ module Foobara
37
37
  end
38
38
  end
39
39
 
40
- attr_accessor :target_classes
40
+ attr_accessor :target_classes, :cast_even_if_instance_of_target_type
41
41
 
42
- def initialize(*, casters:, target_classes: nil, **)
42
+ def initialize(*, casters:, target_classes: nil, cast_even_if_instance_of_target_type: nil, **)
43
43
  self.target_classes = Util.array(target_classes)
44
44
 
45
- processors = [
46
- *does_not_need_cast_processor,
47
- *casters
48
- ]
45
+ if cast_even_if_instance_of_target_type
46
+ self.cast_even_if_instance_of_target_type = true
47
+ end
48
+
49
+ super(*, processors: casters, **)
50
+ end
49
51
 
50
- super(*, processors:, **)
52
+ def process_value(value)
53
+ if cast_even_if_instance_of_target_type || needs_cast?(value)
54
+ super
55
+ else
56
+ Outcome.success(value)
57
+ end
51
58
  end
52
59
 
53
60
  def needs_cast?(value)
54
- !does_not_need_cast_processor.applicable?(value)
61
+ target_classes.none? { |klass| value.is_a?(klass) }
55
62
  end
56
63
 
57
64
  def can_cast?(value)
58
65
  processors.any? { |processor| processor.applicable?(value) }
59
66
  end
60
67
 
61
- def does_not_need_cast_processor
62
- return @does_not_need_cast_processor if defined?(@does_not_need_cast_processor)
63
-
64
- errorified_name = target_classes.map do |c|
65
- if c.name
66
- c.name
67
- elsif c.respond_to?(:foobara_name)
68
- c.foobara_name
69
- else
70
- # TODO: test this code path
71
- # :nocov:
72
- "Anon"
73
- # :nocov:
74
- end
75
- end.map { |name| name.split("::").last }.sort.join("Or")
76
-
77
- class_name = "NoCastNeededIfIsA#{errorified_name}"
78
-
79
- @does_not_need_cast_processor = if target_classes && !target_classes.empty?
80
- Caster.subclass(
81
- name: class_name,
82
- applicable?: ->(value) {
83
- target_classes.any? { |target_class| value.is_a?(target_class) }
84
- },
85
- applies_message: "be a #{target_classes.map(&:name).join(" or ")}",
86
- cast: ->(value) { value }
87
- ).instance
88
- end
89
- end
90
-
91
68
  def error_message(value)
92
69
  type = declaration_data[:cast_to]
93
70
 
@@ -101,7 +78,12 @@ module Foobara
101
78
  end
102
79
 
103
80
  def applies_message
104
- Util.to_or_sentence(processors.map(&:applies_message).flatten)
81
+ Util.to_or_sentence(
82
+ [
83
+ "be a #{target_classes.map(&:name).join(" or ")}",
84
+ *processors.map(&:applies_message).flatten
85
+ ]
86
+ )
105
87
  end
106
88
 
107
89
  def error_context(value)
data/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Foobara
2
2
  module Version
3
- VERSION = "0.1.6".freeze
3
+ VERSION = "0.1.8".freeze
4
4
  MINIMUM_RUBY_VERSION = ">= 3.4.0".freeze
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -304,6 +304,8 @@ files:
304
304
  - projects/entity/src/extensions/builtin_types/entity.rb
305
305
  - projects/entity/src/extensions/builtin_types/entity/casters/hash.rb
306
306
  - projects/entity/src/extensions/builtin_types/entity/casters/primary_key.rb
307
+ - projects/entity/src/extensions/builtin_types/entity/casters/record_from_closed_transaction.rb
308
+ - projects/entity/src/extensions/builtin_types/entity/casters/record_from_current_transaction.rb
307
309
  - projects/entity/src/extensions/builtin_types/entity/validators/model_instance_is_valid.rb
308
310
  - projects/entity/src/extensions/type_declarations/handlers/extend_entity_type_declaration.rb
309
311
  - projects/entity/src/extensions/type_declarations/handlers/extend_entity_type_declaration/attributes_handler_desugarizer.rb