castkit 0.1.2 → 0.2.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +196 -219
  3. data/CHANGELOG.md +42 -0
  4. data/README.md +469 -84
  5. data/lib/castkit/attribute.rb +6 -24
  6. data/lib/castkit/castkit.rb +58 -10
  7. data/lib/castkit/configuration.rb +94 -47
  8. data/lib/castkit/contract/data_object.rb +62 -0
  9. data/lib/castkit/contract/generic.rb +168 -0
  10. data/lib/castkit/contract/result.rb +74 -0
  11. data/lib/castkit/contract/validator.rb +248 -0
  12. data/lib/castkit/contract.rb +67 -0
  13. data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
  14. data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
  15. data/lib/castkit/core/config.rb +74 -0
  16. data/lib/castkit/core/registerable.rb +59 -0
  17. data/lib/castkit/data_object.rb +45 -60
  18. data/lib/castkit/default_serializer.rb +85 -54
  19. data/lib/castkit/error.rb +15 -3
  20. data/lib/castkit/ext/attribute/access.rb +67 -0
  21. data/lib/castkit/ext/attribute/error_handling.rb +63 -0
  22. data/lib/castkit/ext/attribute/options.rb +142 -0
  23. data/lib/castkit/ext/attribute/validation.rb +85 -0
  24. data/lib/castkit/ext/data_object/contract.rb +96 -0
  25. data/lib/castkit/ext/data_object/deserialization.rb +167 -0
  26. data/lib/castkit/ext/data_object/serialization.rb +61 -0
  27. data/lib/castkit/inflector.rb +47 -0
  28. data/lib/castkit/types/boolean.rb +43 -0
  29. data/lib/castkit/types/collection.rb +24 -0
  30. data/lib/castkit/types/date.rb +34 -0
  31. data/lib/castkit/types/date_time.rb +34 -0
  32. data/lib/castkit/types/float.rb +46 -0
  33. data/lib/castkit/types/generic.rb +123 -0
  34. data/lib/castkit/types/integer.rb +46 -0
  35. data/lib/castkit/types/string.rb +44 -0
  36. data/lib/castkit/types.rb +15 -0
  37. data/lib/castkit/validators/base_validator.rb +39 -0
  38. data/lib/castkit/validators/numeric_validator.rb +2 -2
  39. data/lib/castkit/validators/string_validator.rb +3 -3
  40. data/lib/castkit/version.rb +1 -1
  41. data/lib/castkit.rb +2 -0
  42. metadata +29 -13
  43. data/lib/castkit/attribute_extensions/access.rb +0 -65
  44. data/lib/castkit/attribute_extensions/casting.rb +0 -147
  45. data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
  46. data/lib/castkit/attribute_extensions/options.rb +0 -131
  47. data/lib/castkit/attribute_extensions/serialization.rb +0 -89
  48. data/lib/castkit/attribute_extensions/validation.rb +0 -72
  49. data/lib/castkit/data_object_extensions/config.rb +0 -113
  50. data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
  51. data/lib/castkit/validators.rb +0 -4
@@ -5,25 +5,24 @@ require_relative "serializer"
5
5
  module Castkit
6
6
  # Default serializer for Castkit::DataObject instances.
7
7
  #
8
- # Serializes attributes based on access rules, nil/blank filtering, and nested structure.
8
+ # Serializes attributes into a plain Ruby hash, applying access rules, nil/blank filtering,
9
+ # and nested structure handling. The output format supports JSON-compatible structures
10
+ # and respects the class-level serialization configuration.
9
11
  class DefaultSerializer < Castkit::Serializer
10
- SKIP_ATTRIBUTE = :__castkit_skip_attribute
11
-
12
- # @return [Hash{Symbol => Castkit::Attribute}] attributes to serialize
12
+ # @return [Hash{Symbol => Castkit::Attribute}] the attributes to serialize
13
13
  attr_reader :attributes
14
14
 
15
- # @return [Hash{Symbol => Object}] attributes that are not predefined or known to the object
15
+ # @return [Hash{Symbol => Object}] unrecognized attributes captured during deserialization
16
16
  attr_reader :unknown_attributes
17
17
 
18
- # @return [Hash] serialization options (root key, ignore_nil, allow_unknown, etc.)
18
+ # @return [Hash] serialization config flags like :root, :ignore_nil, :allow_unknown
19
19
  attr_reader :options
20
20
 
21
- # Returns the serialized object as a Hash.
21
+ # Serializes the object to a hash.
22
22
  #
23
- # Includes root wrapping if configured. If `allow_unknown` is set to true, unknown attributes
24
- # are merged into the final result.
23
+ # Includes unknown attributes if configured, and wraps in a root key if defined.
25
24
  #
26
- # @return [Hash] The serialized object, potentially wrapped with a root key
25
+ # @return [Hash] the fully serialized result
27
26
  def call
28
27
  result = serialize_attributes
29
28
  result.merge!(unknown_attributes) if options[:allow_unknown]
@@ -33,91 +32,123 @@ module Castkit
33
32
 
34
33
  private
35
34
 
36
- # Initializes the serializer with the target object and context.
35
+ # Initializes the serializer.
37
36
  #
38
- # @param raw [Castkit::DataObject] the object to serialize
39
- # @param visited [Set, nil] used to detect circular references (default is nil)
40
- def initialize(raw, visited: nil)
37
+ # @param obj [Castkit::DataObject] the object to serialize
38
+ # @param visited [Set, nil] tracks circular references
39
+ def initialize(obj, visited: nil)
41
40
  super
42
41
 
43
- # Setting up attributes, unknown attributes, and options based on the class-level configuration
44
- @attributes = raw.class.attributes
45
- @unknown_attributes = raw.unknown_attributes
42
+ @skip_flag = "__castkit_#{obj.object_id}"
43
+ @attributes = obj.class.attributes.freeze
44
+ @unknown_attributes = obj.unknown_attributes.freeze
46
45
  @options = {
47
- root: raw.class.root,
48
- ignore_nil: raw.class.ignore_nil || false,
49
- allow_unknown: raw.class.allow_unknown || false
46
+ root: obj.class.root,
47
+ ignore_nil: obj.class.ignore_nil || false,
48
+ allow_unknown: obj.class.allow_unknown || false
50
49
  }
51
50
  end
52
51
 
53
- # Iterates over attributes and serializes each into a result hash.
52
+ # Serializes all defined attributes.
54
53
  #
55
- # @return [Hash] The serialized attributes as a hash
54
+ # @return [Hash] serialized attribute key-value pairs
56
55
  def serialize_attributes
57
- attributes.each_with_object({}) do |(_, attribute), hash|
56
+ attributes.values.each_with_object({}) do |attribute, hash|
58
57
  next if attribute.skip_serialization?
59
58
 
60
- # Serializing each attribute
61
59
  serialized_value = serialize_attribute(attribute)
62
- next if serialized_value == SKIP_ATTRIBUTE
60
+ next if serialized_value == @skip_flag
63
61
 
64
- # Assign the serialized value to the correct key in the hash
65
- assign_attribute_key!(hash, attribute, serialized_value)
62
+ assign_attribute_key!(attribute, serialized_value, hash)
66
63
  end
67
64
  end
68
65
 
69
- # Process and serialize a given attribute.
70
- #
71
- # This handles value extraction, skipping when nil values are encountered, and ensuring
72
- # attributes are serialized according to their rules.
66
+ # Serializes a single attribute.
73
67
  #
74
- # @param attribute [Castkit::Attribute] The attribute instance to serialize
75
- # @return [Object, nil] The serialized value or SKIP_ATTRIBUTE if the value should be skipped
68
+ # @param attribute [Castkit::Attribute]
69
+ # @return [Object] the serialized value or skip flag
76
70
  def serialize_attribute(attribute)
77
- # Fetch the value of the attribute from the object
78
71
  value = obj.public_send(attribute.field)
72
+ return @skip_flag if skip_nil?(attribute, value)
79
73
 
80
- # Skip serialization if value is nil and ignore_nil is set to true
81
- return SKIP_ATTRIBUTE if value.nil? && (attribute.ignore_nil? || options[:ignore_nil])
82
-
83
- # Serialize the value using the attribute's dump method
84
- serialized_value = attribute.dump(value, visited: visited)
85
-
86
- # Skip if value is blank and ignore_blank is set to true
87
- return SKIP_ATTRIBUTE if blank?(serialized_value) && (attribute.ignore_blank? || options[:ignore_blank])
74
+ serialized_value = process_attribute(attribute, value)
75
+ return @skip_flag if skip_blank?(attribute, serialized_value)
88
76
 
89
77
  serialized_value
90
78
  end
91
79
 
92
- # Assigns a serialized value into the hash using nested key paths.
80
+ # Delegates serialization based on type.
93
81
  #
94
- # This ensures attributes with nested key paths (like `address.city`) are placed into nested hashes.
82
+ # @param attribute [Castkit::Attribute]
83
+ # @param value [Object]
84
+ # @return [Object]
85
+ def process_attribute(attribute, value)
86
+ if attribute.dataobject?
87
+ serialize_dataobject(attribute, value)
88
+ elsif attribute.dataobject_collection?
89
+ Array(value).map { |v| serialize_dataobject(attribute, v) }
90
+ else
91
+ type = Array(attribute.type).first
92
+ Castkit.type_serializer(type).call(value)
93
+ end
94
+ end
95
+
96
+ # Assigns value into nested hash structure based on key path.
95
97
  #
96
- # @param hash [Hash] The resulting hash to populate with the serialized values
97
- # @param attribute [Castkit::Attribute] The attribute being serialized
98
- # @param value [Object] The serialized value
99
- # @return [void] Updates the hash in-place
100
- def assign_attribute_key!(hash, attribute, value)
98
+ # @param attribute [Castkit::Attribute]
99
+ # @param value [Object]
100
+ # @param hash [Hash]
101
+ # @return [void]
102
+ def assign_attribute_key!(attribute, value, hash)
101
103
  key_path = attribute.key_path
102
104
  last = key_path.pop
103
105
  current = hash
104
106
 
105
- # Traverse the key path and create nested hashes as needed
106
107
  key_path.each do |key|
107
108
  current[key] ||= {}
108
109
  current = current[key]
109
110
  end
110
111
 
111
- # Assign the final value to the last key in the path
112
112
  current[last] = value
113
113
  end
114
114
 
115
- # Determines if a value is blank (nil, empty array, empty hash, empty string, etc.)
115
+ # Whether to skip serialization for nil values.
116
+ #
117
+ # @param attribute [Castkit::Attribute]
118
+ # @param value [Object]
119
+ # @return [Boolean]
120
+ def skip_nil?(attribute, value)
121
+ value.nil? && (attribute.ignore_nil? || options[:ignore_nil])
122
+ end
123
+
124
+ # Whether to skip serialization for blank values.
125
+ #
126
+ # @param attribute [Castkit::Attribute]
127
+ # @param value [Object]
128
+ # @return [Boolean]
129
+ def skip_blank?(attribute, value)
130
+ blank?(value) && (attribute.ignore_blank? || options[:ignore_blank])
131
+ end
132
+
133
+ # True if value is nil or empty.
116
134
  #
117
- # @param value [Object, nil] The value to check
118
- # @return [Boolean] true if the value is blank, false otherwise
135
+ # @param value [Object]
136
+ # @return [Boolean]
119
137
  def blank?(value)
120
138
  value.nil? || (value.respond_to?(:empty?) && value.empty?)
121
139
  end
140
+
141
+ # Serializes a DataObject using the proper serializer.
142
+ #
143
+ # @param attribute [Castkit::Attribute]
144
+ # @param value [Castkit::DataObject]
145
+ # @return [Object]
146
+ def serialize_dataobject(attribute, value)
147
+ serializer = attribute.options[:serializer]
148
+ serializer ||= value.class.serializer
149
+ serializer ||= Castkit::DefaultSerializer
150
+
151
+ serializer.call(value, visited: visited)
152
+ end
122
153
  end
123
154
  end
data/lib/castkit/error.rb CHANGED
@@ -9,13 +9,15 @@ module Castkit
9
9
  # Initializes a Castkit error.
10
10
  #
11
11
  # @param msg [String] the error message
12
- # @param context [Object, nil] optional data object or hash for context
12
+ # @param context [Object, String, nil] optional data object or hash for context
13
13
  def initialize(msg, context: nil)
14
14
  super(msg)
15
15
  @context = context
16
16
  end
17
17
  end
18
18
 
19
+ class TypeError < Error; end
20
+
19
21
  # Raised for issues related to Castkit::DataObject initialization or usage.
20
22
  class DataObjectError < Error; end
21
23
 
@@ -23,9 +25,9 @@ module Castkit
23
25
  class AttributeError < Error
24
26
  # Returns the field name related to the error, if available.
25
27
  #
26
- # @return [Symbol]
28
+ # @return [Symbol, nil]
27
29
  def field
28
- context.is_a?(Hash) ? context[:field] : context || :unknown
30
+ context.is_a?(Hash) ? context[:field] : context || nil
29
31
  end
30
32
 
31
33
  # Formats the error message with field info if available.
@@ -39,4 +41,14 @@ module Castkit
39
41
 
40
42
  # Raised during serialization if an object fails to serialize properly.
41
43
  class SerializationError < Error; end
44
+
45
+ # Raised during contract validation.
46
+ class ContractError < Error
47
+ attr_reader :errors
48
+
49
+ def initialize(msg, context: nil, errors: nil)
50
+ super(msg, context: context)
51
+ @errors = errors || {}
52
+ end
53
+ end
42
54
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module Ext
5
+ module Attribute
6
+ # Provides access control helpers for attributes.
7
+ #
8
+ # These helpers determine whether an attribute is readable, writeable,
9
+ # or should be included during serialization/deserialization based on the
10
+ # configured `:access` and `:ignore` options.
11
+ module Access
12
+ # Returns the normalized access modes for the attribute (e.g., [:read, :write]).
13
+ #
14
+ # @return [Array<Symbol>] list of access symbols
15
+ def access
16
+ Array(options[:access]).map(&:to_sym)
17
+ end
18
+
19
+ # Whether the attribute should be included during serialization.
20
+ #
21
+ # @return [Boolean]
22
+ def readable?
23
+ access.include?(:read)
24
+ end
25
+
26
+ # Whether the attribute should be accepted during deserialization.
27
+ #
28
+ # Composite attributes are excluded from writeability.
29
+ #
30
+ # @return [Boolean]
31
+ def writeable?
32
+ access.include?(:write) && !composite?
33
+ end
34
+
35
+ # Whether the attribute is both readable and writeable.
36
+ #
37
+ # @return [Boolean]
38
+ def full_access?
39
+ readable? && writeable?
40
+ end
41
+
42
+ # Whether the attribute should be skipped during serialization.
43
+ #
44
+ # This is true if it's not readable or is marked as ignored.
45
+ #
46
+ # @return [Boolean]
47
+ def skip_serialization?
48
+ !readable? || ignore?
49
+ end
50
+
51
+ # Whether the attribute should be skipped during deserialization.
52
+ #
53
+ # @return [Boolean]
54
+ def skip_deserialization?
55
+ !writeable?
56
+ end
57
+
58
+ # Whether the attribute is ignored completely.
59
+ #
60
+ # @return [Boolean]
61
+ def ignore?
62
+ options[:ignore]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module Ext
5
+ module Attribute
6
+ # Provides centralized handling of attribute casting and validation errors.
7
+ #
8
+ # The behavior of each error is controlled by configuration flags in `Castkit.configuration`.
9
+ module ErrorHandling
10
+ # Maps known error types to their handling behavior.
11
+ #
12
+ # Each entry includes:
13
+ # - `:config` – the config flag that determines enforcement
14
+ # - `:message` – a lambda that generates an error message
15
+ # - `:error` – the error class to raise
16
+ #
17
+ # @return [Hash{Symbol => Hash}]
18
+ ERROR_OPTIONS = {
19
+ access: {
20
+ config: :enforce_attribute_access,
21
+ message: ->(_attr, mode:) { "invalid access mode `#{mode}`" },
22
+ error: Castkit::AttributeError
23
+ },
24
+ unwrapped: {
25
+ config: :enforce_unwrapped_prefix,
26
+ message: ->(*_) { "prefix can only be used with unwrapped attribute" },
27
+ error: Castkit::AttributeError
28
+ },
29
+ array_options: {
30
+ config: :enforce_array_options,
31
+ message: ->(*_) { "array attribute must specify `of:` type" },
32
+ error: Castkit::AttributeError
33
+ }
34
+ }.freeze
35
+
36
+ private
37
+
38
+ # Handles a validation or casting error based on the provided error key and context.
39
+ #
40
+ # If the corresponding configuration flag is enabled, an exception is raised.
41
+ # Otherwise, a warning is logged and the method returns `nil`.
42
+ #
43
+ # @param key [Symbol] the type of error (must match a key in ERROR_OPTIONS)
44
+ # @param kwargs [Hash] additional values passed to the message lambda
45
+ # @option kwargs [Symbol] :context (optional) the attribute context (e.g., field name)
46
+ # @return [nil]
47
+ # @raise [Castkit::AttributeError] if enforcement is enabled for the given error type
48
+ def handle_error(key, **kwargs)
49
+ config_key = ERROR_OPTIONS.dig(key, :config)
50
+ message_fn = ERROR_OPTIONS.dig(key, :message)
51
+ error_class = ERROR_OPTIONS.dig(key, :error) || Castkit::Error
52
+
53
+ context = kwargs.delete(:context)
54
+ message = message_fn.call(self, **kwargs)
55
+ raise error_class.new(message, context: context) if Castkit.configuration.public_send(config_key)
56
+
57
+ Castkit.warning "[Castkit] #{message}"
58
+ nil
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../data_object"
4
+
5
+ module Castkit
6
+ module Ext
7
+ module Attribute
8
+ # Provides access to normalized attribute options and helper predicates.
9
+ #
10
+ # These methods support Castkit attribute behavior such as default values,
11
+ # key mapping, optionality, and structural roles (e.g. composite or unwrapped).
12
+ module Options
13
+ # Default options for attributes.
14
+ #
15
+ # @return [Hash{Symbol => Object}]
16
+ DEFAULT_OPTIONS = {
17
+ required: true,
18
+ ignore_nil: false,
19
+ ignore_blank: false,
20
+ ignore: false,
21
+ composite: false,
22
+ transient: false,
23
+ unwrapped: false,
24
+ prefix: nil,
25
+ access: %i[read write],
26
+ force_type: !Castkit.configuration.enforce_typing
27
+ }.freeze
28
+
29
+ # Returns the default value for the attribute.
30
+ #
31
+ # If the default is callable, it is invoked.
32
+ #
33
+ # @return [Object]
34
+ def default
35
+ val = @default
36
+ val.respond_to?(:call) ? val.call : val
37
+ end
38
+
39
+ # Returns the serialization/deserialization key.
40
+ #
41
+ # Falls back to the field name if `:key` is not specified.
42
+ #
43
+ # @return [Symbol, String]
44
+ def key
45
+ options[:key] || field
46
+ end
47
+
48
+ # Returns the key path for accessing nested keys.
49
+ #
50
+ # Optionally includes alias key paths if `with_aliases` is true.
51
+ #
52
+ # @param with_aliases [Boolean]
53
+ # @return [Array<Array<Symbol>>] nested key paths
54
+ def key_path(with_aliases: false)
55
+ path = key.to_s.split(".").map(&:to_sym) || []
56
+ return path unless with_aliases
57
+
58
+ [path] + alias_paths
59
+ end
60
+
61
+ # Returns all alias key paths as arrays of symbols.
62
+ #
63
+ # @return [Array<Array<Symbol>>]
64
+ def alias_paths
65
+ options[:aliases].map { |a| a.to_s.split(".").map(&:to_sym) }
66
+ end
67
+
68
+ # Whether the attribute is required for object construction.
69
+ #
70
+ # @return [Boolean]
71
+ def required?
72
+ options[:required]
73
+ end
74
+
75
+ # Whether the attribute is optional.
76
+ #
77
+ # @return [Boolean]
78
+ def optional?
79
+ !required?
80
+ end
81
+
82
+ # Whether to ignore `nil` values during serialization.
83
+ #
84
+ # @return [Boolean]
85
+ def ignore_nil?
86
+ options[:ignore_nil]
87
+ end
88
+
89
+ # Whether to ignore blank values (`[]`, `{}`, empty strings) during serialization.
90
+ #
91
+ # @return [Boolean]
92
+ def ignore_blank?
93
+ options[:ignore_blank]
94
+ end
95
+
96
+ # Whether the attribute is a nested Castkit::DataObject.
97
+ #
98
+ # @return [Boolean]
99
+ def dataobject?
100
+ Castkit.dataobject?(type)
101
+ end
102
+
103
+ # Whether the attribute is a collection of Castkit::DataObjects.
104
+ #
105
+ # @return [Boolean]
106
+ def dataobject_collection?
107
+ type == :array && Castkit.dataobject?(options[:of])
108
+ end
109
+
110
+ # Whether the attribute is considered composite.
111
+ #
112
+ # @return [Boolean]
113
+ def composite?
114
+ options[:composite]
115
+ end
116
+
117
+ # Whether the attribute is considered transient (not exposed in serialized output).
118
+ #
119
+ # @return [Boolean]
120
+ def transient?
121
+ options[:transient]
122
+ end
123
+
124
+ # Whether the attribute is unwrapped into the parent object.
125
+ #
126
+ # Only applies to Castkit::DataObject types.
127
+ #
128
+ # @return [Boolean]
129
+ def unwrapped?
130
+ dataobject? && options[:unwrapped]
131
+ end
132
+
133
+ # Returns the prefix used for unwrapped attributes.
134
+ #
135
+ # @return [String, nil]
136
+ def prefix
137
+ options[:prefix]
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error_handling"
4
+ require_relative "options"
5
+
6
+ module Castkit
7
+ module Ext
8
+ module Attribute
9
+ # Provides validation logic for attribute configuration.
10
+ #
11
+ # These checks are typically performed at attribute initialization to catch misconfigurations early.
12
+ module Validation
13
+ include Castkit::Ext::Attribute::ErrorHandling
14
+
15
+ private
16
+
17
+ # Runs all validation checks on the attribute definition.
18
+ #
19
+ # This includes:
20
+ # - Custom validator integrity
21
+ # - Access mode validity
22
+ # - Unwrapped prefix usage
23
+ # - Array `of:` type presence
24
+ #
25
+ # @return [void]
26
+ def validate!
27
+ validate_type!
28
+ validate_custom_validator!
29
+ validate_access!
30
+ validate_unwrapped_options!
31
+ validate_array_options!
32
+ end
33
+
34
+ def validate_type!
35
+ types ||= Array(type) # used to test single type and type unions.
36
+
37
+ types.each do |t|
38
+ next if Castkit.dataobject?(t) || Castkit.configuration.type_registered?(t)
39
+
40
+ raise_error!("Type is not registered, register with Castkit.configuration.register_type(:#{t})")
41
+ end
42
+ end
43
+
44
+ # Validates the presence and interface of a custom validator.
45
+ #
46
+ # @return [void]
47
+ # @raise [Castkit::AttributeError] if the validator is not callable
48
+ def validate_custom_validator!
49
+ return unless options[:validator]
50
+ return if options[:validator].respond_to?(:call)
51
+
52
+ raise_error!("Custom validator for `#{field}` must respond to `.call`")
53
+ end
54
+
55
+ # Validates that each declared access mode is valid.
56
+ #
57
+ # @return [void]
58
+ # @raise [Castkit::AttributeError] if any access mode is invalid and enforcement is enabled
59
+ def validate_access!
60
+ access.each do |mode|
61
+ next if Castkit::Ext::Attribute::Options::DEFAULT_OPTIONS[:access].include?(mode)
62
+
63
+ handle_error(:access, mode: mode, context: to_h)
64
+ end
65
+ end
66
+
67
+ # Ensures prefix is only used with unwrapped attributes.
68
+ #
69
+ # @return [void]
70
+ # @raise [Castkit::AttributeError] if prefix is used without `unwrapped: true`
71
+ def validate_unwrapped_options!
72
+ handle_error(:unwrapped, context: to_h) if prefix && !unwrapped?
73
+ end
74
+
75
+ # Ensures `of:` is provided for array-typed attributes.
76
+ #
77
+ # @return [void]
78
+ # @raise [Castkit::AttributeError] if `of:` is missing for `type: :array`
79
+ def validate_array_options!
80
+ handle_error(:array_options, context: to_h) if type == :array && options[:of].nil?
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end