castkit 0.3.0 → 0.4.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +19 -11
  4. data/castkit.gemspec +4 -0
  5. data/lib/castkit/attribute.rb +87 -65
  6. data/lib/castkit/attributes/definition.rb +64 -0
  7. data/lib/castkit/attributes/options.rb +214 -0
  8. data/lib/castkit/castkit.rb +14 -3
  9. data/lib/castkit/cli/generate.rb +14 -0
  10. data/lib/castkit/configuration.rb +25 -48
  11. data/lib/castkit/contract/base.rb +8 -23
  12. data/lib/castkit/contract/result.rb +10 -6
  13. data/lib/castkit/contract/validator.rb +5 -1
  14. data/lib/castkit/core/attribute_types.rb +3 -1
  15. data/lib/castkit/core/attributes.rb +132 -65
  16. data/lib/castkit/core/config.rb +23 -13
  17. data/lib/castkit/data_object.rb +9 -29
  18. data/lib/castkit/{ext → dsl}/attribute/access.rb +1 -1
  19. data/lib/castkit/{ext → dsl}/attribute/error_handling.rb +1 -1
  20. data/lib/castkit/{ext → dsl}/attribute/options.rb +1 -1
  21. data/lib/castkit/{ext → dsl}/attribute/validation.rb +3 -3
  22. data/lib/castkit/dsl/attribute.rb +47 -0
  23. data/lib/castkit/{ext → dsl}/data_object/contract.rb +1 -1
  24. data/lib/castkit/{ext → dsl}/data_object/deserialization.rb +24 -3
  25. data/lib/castkit/dsl/data_object/introspection.rb +52 -0
  26. data/lib/castkit/{ext → dsl}/data_object/plugins.rb +1 -1
  27. data/lib/castkit/{ext → dsl}/data_object/serialization.rb +5 -2
  28. data/lib/castkit/dsl/data_object.rb +65 -0
  29. data/lib/castkit/error.rb +8 -4
  30. data/lib/castkit/plugins.rb +12 -3
  31. data/lib/castkit/serializers/base.rb +9 -4
  32. data/lib/castkit/serializers/default_serializer.rb +10 -10
  33. data/lib/castkit/types/base.rb +24 -3
  34. data/lib/castkit/validators/boolean_validator.rb +3 -3
  35. data/lib/castkit/validators/collection_validator.rb +2 -2
  36. data/lib/castkit/version.rb +1 -1
  37. data/lib/castkit.rb +1 -4
  38. data/lib/generators/attribute.rb +39 -0
  39. data/lib/generators/templates/attribute.rb.tt +21 -0
  40. data/lib/generators/templates/attribute_spec.rb.tt +41 -0
  41. data/lib/generators/templates/contract.rb.tt +2 -0
  42. data/lib/generators/templates/data_object.rb.tt +2 -0
  43. data/lib/generators/templates/type.rb.tt +2 -0
  44. data/lib/generators/templates/validator.rb.tt +1 -1
  45. metadata +74 -12
  46. data/.rspec_status +0 -195
  47. data/lib/castkit/core/registerable.rb +0 -59
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "types"
4
+ require "cattri"
4
5
 
5
6
  module Castkit
6
7
  # Configuration container for global Castkit settings.
@@ -8,6 +9,8 @@ module Castkit
8
9
  # This includes type registration, validation, and enforcement flags
9
10
  # used throughout Castkit's attribute system.
10
11
  class Configuration
12
+ include Cattri
13
+
11
14
  # Default mapping of primitive type definitions.
12
15
  #
13
16
  # @return [Hash{Symbol => Castkit::Types::Base}]
@@ -36,56 +39,30 @@ module Castkit
36
39
  uuid: :string
37
40
  }.freeze
38
41
 
39
- # @return [Hash{Symbol => Castkit::Types::Base}] registered types
40
- attr_reader :types
41
-
42
- # Set default plugins that will be used globally in all Castkit::DataObject subclasses.
43
- # This is equivalent to calling `enable_plugins` in every class.
44
- #
45
- # @return [Array<Symbol>] default plugin names to be applied to all DataObject subclasses
46
- attr_accessor :default_plugins
47
-
48
- # Whether to raise an error if values should be validated before deserializing, e.g. true -> "true"
49
- # @return [Boolean]
50
- attr_accessor :enforce_typing
51
-
52
- # Whether to raise an error if access mode is not recognized.
53
- # @return [Boolean]
54
- attr_accessor :enforce_attribute_access
55
-
56
- # Whether to raise an error if a prefix is defined without `unwrapped: true`.
57
- # @return [Boolean]
58
- attr_accessor :enforce_unwrapped_prefix
59
-
60
- # Whether to raise an error if an array attribute is missing the `of:` type.
61
- # @return [Boolean]
62
- attr_accessor :enforce_array_options
63
-
64
- # Whether to raise an error for unknown and invalid type definitions.
65
- # @return [Boolean]
66
- attr_accessor :raise_type_errors
67
-
68
- # Whether to emit warnings when Castkit detects misconfigurations.
69
- # @return [Boolean]
70
- attr_accessor :enable_warnings
71
-
72
- # Whether the strict flag is enabled by default for all DataObjects and Contracts.
73
- # @return [Boolean]
74
- attr_accessor :strict_by_default
42
+ cattri :types, -> { DEFAULT_TYPES.dup }, expose: :read_write
43
+ cattri :default_plugins, [], expose: :read_write
44
+ cattri :enforce_typing, true, expose: :read_write
45
+ cattri :enforce_attribute_access, true, expose: :read_write
46
+ cattri :enforce_unwrapped_prefix, true, expose: :read_write
47
+ cattri :enforce_array_options, true, expose: :read_write
48
+ cattri :raise_type_errors, true, expose: :read_write
49
+ cattri :enable_warnings, true, expose: :read_write
50
+ cattri :strict_by_default, true, expose: :read_write
75
51
 
76
52
  # Initializes the configuration with default types and enforcement flags.
77
53
  #
78
54
  # @return [void]
79
55
  def initialize
80
- @types = DEFAULT_TYPES.dup
81
- @enforce_typing = true
82
- @enforce_attribute_access = true
83
- @enforce_unwrapped_prefix = true
84
- @enforce_array_options = true
85
- @raise_type_errors = true
86
- @enable_warnings = true
87
- @strict_by_default = true
88
- @default_plugins = []
56
+ super
57
+ self.types = DEFAULT_TYPES.dup
58
+ self.enforce_typing = true
59
+ self.enforce_attribute_access = true
60
+ self.enforce_unwrapped_prefix = true
61
+ self.enforce_array_options = true
62
+ self.raise_type_errors = true
63
+ self.enable_warnings = true
64
+ self.strict_by_default = true
65
+ self.default_plugins = []
89
66
 
90
67
  apply_type_aliases!
91
68
  end
@@ -136,7 +113,7 @@ module Castkit
136
113
  # @return [Castkit::Types::Base]
137
114
  # @raise [Castkit::TypeError] if the type is not registered
138
115
  def fetch_type(type)
139
- @types.fetch(type.to_sym) do
116
+ types.fetch(type.to_sym) do
140
117
  raise Castkit::TypeError, "Unknown type `#{type.inspect}`" if raise_type_errors
141
118
  end
142
119
  end
@@ -146,14 +123,14 @@ module Castkit
146
123
  # @param type [Symbol]
147
124
  # @return [Boolean]
148
125
  def type_registered?(type)
149
- @types.key?(type.to_sym)
126
+ types.key?(type.to_sym)
150
127
  end
151
128
 
152
129
  # Restores the type registry to its default state.
153
130
  #
154
131
  # @return [void]
155
132
  def reset_types!
156
- @types = DEFAULT_TYPES.dup
133
+ self.types = DEFAULT_TYPES.dup
157
134
  apply_type_aliases!
158
135
  end
159
136
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative "../core/config"
4
4
  require_relative "../core/attribute_types"
5
- require_relative "../core/registerable"
6
5
  require_relative "result"
7
6
 
8
7
  module Castkit
@@ -33,22 +32,13 @@ module Castkit
33
32
  class Base
34
33
  extend Castkit::Core::Config
35
34
  extend Castkit::Core::AttributeTypes
36
- extend Castkit::Core::Registerable
37
35
 
38
36
  ATTRIBUTE_OPTIONS = %i[
39
37
  required aliases min max format of validator unwrapped prefix force_type
40
38
  ].freeze
41
39
 
42
40
  class << self
43
- # Registers the current class under `Castkit::Contracts`.
44
- #
45
- # @param as [String, Symbol, nil] The constant name to use (PascalCase). Defaults to the name used when building
46
- # the contract. If no name was provided, an error is raised.
47
- # @return [Class] the registered contract class
48
- # @raise [Castkit::Error] If a name cannot be resolved.
49
- def register!(as: nil)
50
- super(namespace: :contracts, as: as || definition[:name])
51
- end
41
+ include Cattri
52
42
 
53
43
  # Defines an attribute for the contract.
54
44
  #
@@ -83,15 +73,9 @@ module Castkit
83
73
  Castkit::Contract::Result.new(definition[:name].to_s, input)
84
74
  end
85
75
 
86
- # Returns internal contract metadata.
87
- #
88
- # @return [Hash]
89
- def definition
90
- @definition ||= {
91
- name: :ephemeral,
92
- attributes: {}
93
- }
94
- end
76
+ cattri :definition,
77
+ -> { { name: :ephemeral, attributes: {} } },
78
+ scope: :class, expose: :read_write
95
79
 
96
80
  # Returns the defined attributes.
97
81
  #
@@ -129,8 +113,8 @@ module Castkit
129
113
  def define_from_source(name, source)
130
114
  source_attributes = source.attributes.dup
131
115
 
132
- @definition = {
133
- name: name,
116
+ self.definition = {
117
+ name: name.to_sym,
134
118
  attributes: source_attributes.transform_values do |attr|
135
119
  Castkit::Attribute.new(attr.field, attr.type, **attr.options.slice(*ATTRIBUTE_OPTIONS))
136
120
  end
@@ -143,7 +127,8 @@ module Castkit
143
127
  # @yield [block]
144
128
  # @return [void]
145
129
  def define_from_block(name, &block)
146
- definition[:name] = name
130
+ contract_name = name ? name.to_sym : :ephemeral
131
+ self.definition = { name: contract_name, attributes: {} }
147
132
 
148
133
  @__castkit_contract_dsl = true
149
134
  instance_eval(&block)
@@ -7,14 +7,16 @@ module Castkit
7
7
  # Provides access to the validation outcome, including whether it succeeded or failed,
8
8
  # and includes the full list of errors if any.
9
9
  class Result
10
+ include Cattri
11
+
10
12
  # @return [Symbol] the name of the contract
11
- attr_reader :contract
13
+ cattri :contract, nil, expose: :read
12
14
 
13
15
  # @return [Hash{Symbol => Object}] the validated input
14
- attr_reader :input
16
+ cattri :input, nil, expose: :read
15
17
 
16
18
  # @return [Hash{Symbol => Object}] the validation error hash
17
- attr_reader :errors
19
+ cattri :errors, {}, expose: :read
18
20
 
19
21
  # Initializes a new result object.
20
22
  #
@@ -22,9 +24,11 @@ module Castkit
22
24
  # @param input [Hash{Symbol => Object}] the validated input
23
25
  # @param errors [Hash{Symbol => Object}] the validation errors
24
26
  def initialize(contract, input, errors: {})
25
- @contract = contract.to_sym.freeze
26
- @input = input.freeze
27
- @errors = errors.freeze
27
+ super()
28
+
29
+ cattri_variable_set(:contract, contract.to_sym.freeze)
30
+ cattri_variable_set(:input, input.freeze)
31
+ cattri_variable_set(:errors, errors.freeze)
28
32
  end
29
33
 
30
34
  # A debug-friendly representation of the validation result.
@@ -188,7 +188,11 @@ module Castkit
188
188
  # @return [Object, nil]
189
189
  def resolve_input_value(input, attribute)
190
190
  attribute.key_path(with_aliases: true).each do |path|
191
- value = path.reduce(input) { |memo, key| memo.is_a?(Hash) ? memo[key] : nil }
191
+ value = path.reduce(input) do |memo, key|
192
+ next memo unless memo.is_a?(Hash)
193
+
194
+ memo.key?(key) ? memo[key] : memo[key.to_s]
195
+ end
192
196
  return value unless value.nil?
193
197
  end
194
198
 
@@ -86,7 +86,9 @@ module Castkit
86
86
  #
87
87
  # @param field [Symbol]
88
88
  # @param options [Hash]
89
- def hash(field, **options)
89
+ def hash(field = nil, **options)
90
+ return super() if field.nil?
91
+
90
92
  attribute(field, :hash, **options)
91
93
  end
92
94
 
@@ -1,48 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cattri"
4
+
3
5
  module Castkit
4
6
  module Core
5
7
  # Provides DSL and implementation for declaring attributes within a Castkit::DataObject.
6
8
  #
7
- # Includes support for regular, composite, transient, readonly/writeonly, and grouped attribute definitions.
8
- module Attributes
9
- # Declares an attribute with the given type and options.
9
+ # Supports reusable attribute definitions, transient fields, composite readers, and
10
+ # grouped declarations such as `readonly`, `optional`, and `transient` blocks.
11
+ #
12
+ # This module is included into `Castkit::DataObject` and handles attribute registration,
13
+ # accessor generation, and typed writing behavior.
14
+ module Attributes # rubocop:disable Metrics/ModuleLength
15
+ def self.extended(base)
16
+ base.include(Cattri)
17
+ end
18
+
19
+ # Declares an attribute on the data object.
10
20
  #
21
+ # Accepts either inline options or a reusable attribute definition (`using` or `definition`).
11
22
  # If `:transient` is true, defines only standard accessors and skips serialization logic.
12
23
  #
13
- # @param field [Symbol]
14
- # @param type [Symbol, Class]
15
- # @param options [Hash]
24
+ # @param field [Symbol] the attribute name
25
+ # @param type [Symbol, Class] the attribute's declared type
26
+ # @param definition [Hash, nil] an optional pre-built definition object (`{ type:, options: }`)
27
+ # @param using [Castkit::Attributes::Base, nil] an optional class-based definition (`.definition`)
28
+ # @param options [Hash] additional options like `default`, `access`, `required`, etc.
16
29
  # @return [void]
17
- # @raise [Castkit::DataObjectError] if the attribute is already defined
18
- def attribute(field, type, **options)
30
+ # @raise [Castkit::DataObjectError] if attribute already defined or type mismatch
31
+ def attribute(field, type = nil, definition = nil, using: nil, **options)
19
32
  field = field.to_sym
20
33
  raise Castkit::DataObjectError, "Attribute '#{field}' already defined" if attributes.key?(field)
21
34
 
22
- options = build_options(options)
35
+ type, options = use_definition(field, definition || using&.definition, type, options)
23
36
  return define_attribute(field, type, **options) unless options[:transient]
24
37
 
25
- attr_accessor field
38
+ define_transient_accessor(field)
26
39
  end
27
40
 
28
- # Declares a computed (composite) attribute.
29
- #
30
- # The provided block defines the read behavior.
41
+ # Declares a composite (computed) attribute.
31
42
  #
32
- # @param field [Symbol]
33
- # @param type [Symbol, Class]
34
- # @param options [Hash]
35
- # @yieldreturn [Object] evaluated composite value
43
+ # @param field [Symbol] the name of the attribute
44
+ # @param type [Symbol, Class] the attribute type
45
+ # @param options [Hash] additional attribute options
46
+ # @yieldreturn [Object] the value to return when the reader is called
47
+ # @return [void]
36
48
  def composite(field, type, **options, &block)
37
49
  attribute(field, type, **options, composite: true)
38
50
  define_method(field, &block)
39
51
  end
40
52
 
41
- # Declares a group of transient attributes within the given block.
53
+ # Declares a group of transient attributes within a block.
42
54
  #
43
- # These attributes are not serialized or included in `to_h`.
55
+ # These attributes are excluded from serialization (`to_h`) and not stored.
44
56
  #
45
- # @yield defines one or more transient attributes via `attribute`
57
+ # @yield a block containing `attribute` calls
46
58
  # @return [void]
47
59
  def transient(&block)
48
60
  @__transient_context = true
@@ -51,126 +63,161 @@ module Castkit
51
63
  @__transient_context = nil
52
64
  end
53
65
 
54
- # Declares a group of readonly attributes within the given block.
66
+ # Declares a group of readonly attributes (accessible for read only).
55
67
  #
56
- # @param options [Hash] shared options for attributes inside the block
57
- # @yield defines attributes with `access: [:read]`
68
+ # @param options [Hash] shared options for all attributes inside the block
69
+ # @yield a block containing `attribute` calls
58
70
  # @return [void]
59
71
  def readonly(**options, &block)
60
72
  with_access([:read], options, &block)
61
73
  end
62
74
 
63
- # Declares a group of writeonly attributes within the given block.
75
+ # Declares a group of writeonly attributes (accessible for write only).
64
76
  #
65
- # @param options [Hash] shared options for attributes inside the block
66
- # @yield defines attributes with `access: [:write]`
77
+ # @param options [Hash] shared options for all attributes inside the block
78
+ # @yield a block containing `attribute` calls
67
79
  # @return [void]
68
80
  def writeonly(**options, &block)
69
81
  with_access([:write], options, &block)
70
82
  end
71
83
 
72
- # Declares a group of required attributes within the given block.
84
+ # Declares a group of required attributes.
73
85
  #
74
- # @param options [Hash] shared options for attributes inside the block
75
- # @yield defines attributes with `required: true`
86
+ # @param options [Hash] shared options for all attributes inside the block
87
+ # @yield a block containing `attribute` calls
76
88
  # @return [void]
77
89
  def required(**options, &block)
78
90
  with_required(true, options, &block)
79
91
  end
80
92
 
81
- # Declares a group of optional attributes within the given block.
93
+ # Declares a group of optional attributes.
82
94
  #
83
- # @param options [Hash] shared options for attributes inside the block
84
- # @yield defines attributes with `required: false`
95
+ # @param options [Hash] shared options for all attributes inside the block
96
+ # @yield a block containing `attribute` calls
85
97
  # @return [void]
86
98
  def optional(**options, &block)
87
99
  with_required(false, options, &block)
88
100
  end
89
101
 
90
- # Returns all declared non-transient attributes.
102
+ # Returns all non-transient attributes defined on the class.
91
103
  #
92
104
  # @return [Hash{Symbol => Castkit::Attribute}]
93
105
  def attributes
94
- @attributes ||= {}
106
+ cattri_variable_memoize(:__castkit_attributes_registry) { {} }
95
107
  end
96
108
 
97
- # Alias for `composite`
109
+ def inherited(subclass)
110
+ super
111
+
112
+ parent_attributes = cattri_variable_get(:__castkit_attributes_registry)
113
+ subclass.cattri_variable_set(:__castkit_attributes_registry, parent_attributes.dup) if parent_attributes
114
+ end
115
+
116
+ # Alias for {#attribute}
117
+ #
118
+ # @see #attribute
119
+ alias attr attribute
120
+
121
+ # Alias for {#composite}
98
122
  #
99
123
  # @see #composite
100
124
  alias property composite
101
125
 
102
126
  private
103
127
 
104
- # Defines a full attribute, including accessor methods and type logic.
128
+ # Applies a reusable definition to the current attribute call.
129
+ #
130
+ # Ensures the declared type matches and merges options.
131
+ #
132
+ # @param field [Symbol] the attribute name
133
+ # @param definition [Hash{Symbol => Object}, nil]
134
+ # @param type [Symbol, Class]
135
+ # @param options [Hash]
136
+ # @return [Array<(Symbol, Hash)>] the final type and options
137
+ # @raise [Castkit::DataObjectError] if type mismatch occurs
138
+ def use_definition(field, definition, type, options)
139
+ type ||= definition&.fetch(:type, nil)
140
+ raise Castkit::AttributeError, "Attribute `#{field} has no type" if type.nil?
141
+
142
+ if definition && type != definition[:type]
143
+ raise Castkit::AttributeError,
144
+ "Attribute `#{field}` type mismatch: expected #{definition[:type].inspect}, got #{type.inspect}"
145
+ end
146
+
147
+ options = definition[:options].merge(options) if definition
148
+ [type, build_options(options)]
149
+ end
150
+
151
+ # Instantiates and stores a Castkit::Attribute, defining accessors as needed.
105
152
  #
106
153
  # @param field [Symbol]
107
154
  # @param type [Symbol, Class]
108
155
  # @param options [Hash]
156
+ # @return [void]
109
157
  def define_attribute(field, type, **options)
110
158
  attribute = Castkit::Attribute.new(field, type, **options)
111
159
  attributes[field] = attribute
112
160
 
113
- if attribute.full_access?
114
- attr_reader field
115
-
116
- define_typed_writer(field, attribute)
117
- elsif attribute.writeable?
118
- define_typed_writer(field, attribute)
119
- elsif attribute.readable?
120
- attr_reader field
121
- end
161
+ define_accessors(attribute)
122
162
  end
123
163
 
124
- # Defines a type-aware writer method for the attribute.
164
+ # Creates readers/writers for a defined attribute using Cattri.
125
165
  #
126
- # @param field [Symbol]
127
166
  # @param attribute [Castkit::Attribute]
128
- def define_typed_writer(field, attribute)
129
- define_method("#{field}=") do |value|
130
- deserialized_value = Castkit.type_caster(attribute.type).call(
131
- value,
132
- options: attribute.options,
133
- context: attribute.field
134
- )
135
-
136
- instance_variable_set("@#{field}", deserialized_value)
167
+ # @return [void]
168
+ def define_accessors(attribute)
169
+ expose = exposure_for(attribute)
170
+ return if expose == :none
171
+
172
+ if attribute.writeable?
173
+ cattri(attribute.field, nil, expose: expose) do |value|
174
+ Castkit.type_caster(attribute.type.to_sym).call(
175
+ value,
176
+ options: attribute.options,
177
+ context: attribute.field
178
+ )
179
+ end
180
+ else
181
+ cattri(attribute.field, nil, expose: expose)
137
182
  end
138
183
  end
139
184
 
140
- # Applies scoped access control to all attributes declared in the given block.
185
+ # Applies a temporary `access` context to all attributes within a block.
141
186
  #
142
- # @param access [Array<Symbol>] e.g., [:read] or [:write]
187
+ # @param access [Array<Symbol>] e.g. `[:read]` or `[:write]`
143
188
  # @param options [Hash]
144
- # @yield the block containing one or more `attribute` calls
189
+ # @yield the block containing `attribute` calls
190
+ # @return [void]
145
191
  def with_access(access, options = {}, &block)
146
192
  @__access_context = access
147
193
  @__block_options = options
194
+
148
195
  instance_eval(&block)
149
196
  ensure
150
197
  @__access_context = nil
151
198
  @__block_options = nil
152
199
  end
153
200
 
154
- # Applies scoped required/optional flag to all attributes declared in the given block.
201
+ # Applies a temporary `required` context to all attributes within a block.
155
202
  #
156
203
  # @param flag [Boolean]
157
204
  # @param options [Hash]
158
- # @yield the block containing one or more `attribute` calls
205
+ # @yield the block containing `attribute` calls
206
+ # @return [void]
159
207
  def with_required(flag, options = {}, &block)
160
208
  @__required_context = flag
161
209
  @__block_options = options
210
+
162
211
  instance_eval(&block)
163
212
  ensure
164
213
  @__required_context = nil
165
214
  @__block_options = nil
166
215
  end
167
216
 
168
- # Builds effective options for the current attribute definition.
169
- #
170
- # Merges scoped flags like `required`, `access`, and `transient` if present.
217
+ # Merges any current context flags (e.g., required, access) into the options hash.
171
218
  #
172
219
  # @param options [Hash]
173
- # @return [Hash]
220
+ # @return [Hash] effective options for the attribute
174
221
  def build_options(options)
175
222
  base = @__block_options || {}
176
223
  base = base.merge(required: @__required_context) unless @__required_context.nil?
@@ -179,6 +226,26 @@ module Castkit
179
226
 
180
227
  base.merge(options)
181
228
  end
229
+
230
+ # Maps Castkit access flags onto Cattri's expose option.
231
+ #
232
+ # @param attribute [Castkit::Attribute]
233
+ # @return [Symbol]
234
+ def exposure_for(attribute)
235
+ return :read_write if attribute.full_access?
236
+ return :write if attribute.writeable?
237
+ return :read if attribute.readable?
238
+
239
+ :none
240
+ end
241
+
242
+ # Defines read/write accessors for transient attributes.
243
+ #
244
+ # @param field [Symbol]
245
+ # @return [void]
246
+ def define_transient_accessor(field)
247
+ cattri(field, nil, expose: :read_write)
248
+ end
182
249
  end
183
250
  end
184
251
  end
@@ -5,6 +5,17 @@ module Castkit
5
5
  # Provides per-class configuration for a Castkit::DataObject,
6
6
  # including root key handling, strict mode, and unknown key behavior.
7
7
  module Config
8
+ def self.extended(base)
9
+ super
10
+
11
+ base.include(Cattri) unless base.is_a?(Class) && base < Cattri
12
+ return unless base.respond_to?(:cattri)
13
+
14
+ base.cattri :strict_flag, nil, scope: :class, expose: :read_write
15
+ base.cattri :warn_on_unknown_flag, nil, scope: :class, expose: :read_write
16
+ base.cattri :allow_unknown_flag, nil, scope: :class, expose: :read_write
17
+ end
18
+
8
19
  # Sets or retrieves strict mode behavior.
9
20
  #
10
21
  # In strict mode, unknown keys during deserialization raise errors. If unset, falls back
@@ -13,11 +24,9 @@ module Castkit
13
24
  # @param value [Boolean, nil]
14
25
  # @return [Boolean]
15
26
  def strict(value = nil)
16
- if value.nil?
17
- @strict.nil? ? Castkit.configuration.strict_by_default : @strict
18
- else
19
- @strict = !!value
20
- end
27
+ return (strict_flag.nil? ? Castkit.configuration.strict_by_default : strict_flag) if value.nil?
28
+
29
+ self.strict_flag = !!value
21
30
  end
22
31
 
23
32
  # Enables or disables ignoring unknown keys during deserialization.
@@ -27,7 +36,7 @@ module Castkit
27
36
  # @param value [Boolean]
28
37
  # @return [void]
29
38
  def ignore_unknown(value = nil)
30
- @strict = !value
39
+ self.strict_flag = !value
31
40
  end
32
41
 
33
42
  # Sets or retrieves whether to emit warnings when unknown keys are encountered.
@@ -35,7 +44,7 @@ module Castkit
35
44
  # @param value [Boolean, nil]
36
45
  # @return [Boolean, nil]
37
46
  def warn_on_unknown(value = nil)
38
- value.nil? ? @warn_on_unknown : (@warn_on_unknown = value)
47
+ value.nil? ? warn_on_unknown_flag : (self.warn_on_unknown_flag = value)
39
48
  end
40
49
 
41
50
  # Sets or retrieves whether to allow unknown keys when they are encountered.
@@ -43,7 +52,7 @@ module Castkit
43
52
  # @param value [Boolean, nil]
44
53
  # @return [Boolean, nil]
45
54
  def allow_unknown(value = nil)
46
- value.nil? ? @allow_unknown : (@allow_unknown = value)
55
+ value.nil? ? allow_unknown_flag : (self.allow_unknown_flag = value)
47
56
  end
48
57
 
49
58
  # Returns a relaxed version of the current class with strict mode off.
@@ -63,11 +72,12 @@ module Castkit
63
72
  #
64
73
  # @return [Hash{Symbol => Boolean}]
65
74
  def validation_rules
66
- @validation_rules ||= {
67
- strict: strict,
68
- allow_unknown: allow_unknown,
69
- warn_on_unknown: warn_on_unknown
70
- }
75
+ @validation_rules ||= {}
76
+ @validation_rules[:strict] = strict
77
+ @validation_rules[:allow_unknown] = allow_unknown
78
+ @validation_rules[:warn_on_unknown] = warn_on_unknown
79
+
80
+ @validation_rules
71
81
  end
72
82
  end
73
83
  end