castkit 0.3.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f048458a9f967984b6b590c1e6bf2348a43c90936374b4d8755c6f7151057fe8
4
- data.tar.gz: 182e73cec494a329bc670999cceab4a3c1e057eb6108097c759989102232ec94
3
+ metadata.gz: 1202666103d91222c67462e90710b701128129cdbdb2b1363ed5be3603b04cc9
4
+ data.tar.gz: a64630cb20e6cca047cb3b5c0a68a251b33fdd92265d077aaf9c04e7e501ab81
5
5
  SHA512:
6
- metadata.gz: 4d8bc7bdd0ed6c26f7fb7a621306995b4fbfbbe6d1d331ffa813ffc4c13a2d2795b72af9f977bc618e950c8dfdc5b017bb95563a436520e4e545efdf1a9a0b50
7
- data.tar.gz: 196ed35dd105ad854ac74fa4540cbfbb04de30885069719ab80bb84d5a41b8ea8e64df1e085753477dab3bf54f998db20684b5cd6460af4c87447594c320edd3
6
+ metadata.gz: b8d1f485109363ccab35aa46cf605d0c06d96b31257168878422e89bf19c4e44c315fb5481fef72b45192cc133258621657b82a98fc173da2f755d541eabc3a8
7
+ data.tar.gz: 841d82b75c79858208296e300e86b9e4405546d90df7e7cee1cf6071b3811f7c3e05abcb43e78962ae07a01f681ff82817a40b98631590ff960abb2d4e30c5a8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ # v0.4.0
4
+
5
+ - Refactored Castkit internals to build on Cattri registries for attributes, configuration, contracts, and attribute definitions
6
+ - Added Cattri-powered introspection helpers, serialization toggle behavior and enhanced type/validator DSLs, plus coverage for the new plugin helpers
7
+
3
8
  ## [0.3.0] - 2025-04-16
4
9
 
5
10
  ### Added
data/README.md CHANGED
@@ -60,14 +60,14 @@ Castkit comes with built-in support for primitive types and allows registration
60
60
 
61
61
  ```ruby
62
62
  {
63
- array: Castkit::Types::Collection,
64
- boolean: Castkit::Types::Boolean,
65
- date: Castkit::Types::Date,
66
- datetime: Castkit::Types::DateTime,
67
- float: Castkit::Types::Float,
68
- hash: Castkit::Types::Base,
69
- integer: Castkit::Types::Integer,
70
- string: Castkit::Types::String
63
+ array: Castkit::Types::Collection,
64
+ boolean: Castkit::Types::Boolean,
65
+ date: Castkit::Types::Date,
66
+ datetime: Castkit::Types::DateTime,
67
+ float: Castkit::Types::Float,
68
+ hash: Castkit::Types::Base,
69
+ integer: Castkit::Types::Integer,
70
+ string: Castkit::Types::String
71
71
  }
72
72
  ```
73
73
 
data/castkit.gemspec CHANGED
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.require_paths = ["lib"]
34
34
 
35
35
  # Runtime dependencies
36
+ spec.add_dependency "cattri", ">=0.2.3"
36
37
  spec.add_dependency "thor"
37
38
 
38
39
  # Development dependencies
@@ -40,5 +41,6 @@ Gem::Specification.new do |spec|
40
41
  spec.add_development_dependency "rubocop"
41
42
  spec.add_development_dependency "simplecov"
42
43
  spec.add_development_dependency "simplecov-cobertura"
44
+ spec.add_development_dependency "simplecov-html"
43
45
  spec.add_development_dependency "yard"
44
46
  end
@@ -23,6 +23,11 @@ module Castkit
23
23
  # @see Castkit::DSL::Attribute::Validation
24
24
  class Attribute
25
25
  include Castkit::DSL::Attribute
26
+ include Cattri
27
+
28
+ cattri :field, nil, expose: :read, final: true
29
+ cattri :type, nil, expose: :read, final: true
30
+ cattri :options, nil, expose: :read, final: true
26
31
 
27
32
  class << self
28
33
  # Defines a reusable attribute definition via a DSL wrapper.
@@ -71,15 +76,6 @@ module Castkit
71
76
  end
72
77
  end
73
78
 
74
- # @return [Symbol] the attribute name
75
- attr_reader :field
76
-
77
- # @return [Symbol, Class, Array] the declared or normalized type
78
- attr_reader :type
79
-
80
- # @return [Hash] full option hash, including merged defaults
81
- attr_reader :options
82
-
83
79
  # Initializes a new attribute definition.
84
80
  #
85
81
  # @param field [Symbol] the attribute name
@@ -87,10 +83,13 @@ module Castkit
87
83
  # @param default [Object, Proc, nil] optional static or callable default
88
84
  # @param options [Hash] additional attribute options
89
85
  def initialize(field, type, default: nil, **options)
90
- @field = field
91
- @type = self.class.normalize_type(type)
86
+ super()
87
+
88
+ cattri_variable_set(:field, field, final: true)
89
+ cattri_variable_set(:type, self.class.normalize_type(type), final: true)
90
+
92
91
  @default = default
93
- @options = populate_options(options)
92
+ cattri_variable_set(:options, populate_options(options), final: true)
94
93
 
95
94
  validate!
96
95
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cattri"
3
4
  require_relative "core/attribute_types"
4
5
 
5
6
  # Castkit is a lightweight, type-safe data object system for Ruby.
@@ -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
 
@@ -38,6 +38,8 @@ module Castkit
38
38
  ].freeze
39
39
 
40
40
  class << self
41
+ include Cattri
42
+
41
43
  # Defines an attribute for the contract.
42
44
  #
43
45
  # Only a subset of options is allowed inside a contract.
@@ -71,15 +73,9 @@ module Castkit
71
73
  Castkit::Contract::Result.new(definition[:name].to_s, input)
72
74
  end
73
75
 
74
- # Returns internal contract metadata.
75
- #
76
- # @return [Hash]
77
- def definition
78
- @definition ||= {
79
- name: :ephemeral,
80
- attributes: {}
81
- }
82
- end
76
+ cattri :definition,
77
+ -> { { name: :ephemeral, attributes: {} } },
78
+ scope: :class, expose: :read_write
83
79
 
84
80
  # Returns the defined attributes.
85
81
  #
@@ -117,8 +113,8 @@ module Castkit
117
113
  def define_from_source(name, source)
118
114
  source_attributes = source.attributes.dup
119
115
 
120
- @definition = {
121
- name: name,
116
+ self.definition = {
117
+ name: name.to_sym,
122
118
  attributes: source_attributes.transform_values do |attr|
123
119
  Castkit::Attribute.new(attr.field, attr.type, **attr.options.slice(*ATTRIBUTE_OPTIONS))
124
120
  end
@@ -131,7 +127,8 @@ module Castkit
131
127
  # @yield [block]
132
128
  # @return [void]
133
129
  def define_from_block(name, &block)
134
- definition[:name] = name
130
+ contract_name = name ? name.to_sym : :ephemeral
131
+ self.definition = { name: contract_name, attributes: {} }
135
132
 
136
133
  @__castkit_contract_dsl = true
137
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.
@@ -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,5 +1,7 @@
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.
@@ -9,7 +11,11 @@ module Castkit
9
11
  #
10
12
  # This module is included into `Castkit::DataObject` and handles attribute registration,
11
13
  # accessor generation, and typed writing behavior.
12
- module Attributes
14
+ module Attributes # rubocop:disable Metrics/ModuleLength
15
+ def self.extended(base)
16
+ base.include(Cattri)
17
+ end
18
+
13
19
  # Declares an attribute on the data object.
14
20
  #
15
21
  # Accepts either inline options or a reusable attribute definition (`using` or `definition`).
@@ -29,7 +35,7 @@ module Castkit
29
35
  type, options = use_definition(field, definition || using&.definition, type, options)
30
36
  return define_attribute(field, type, **options) unless options[:transient]
31
37
 
32
- attr_accessor field
38
+ define_transient_accessor(field)
33
39
  end
34
40
 
35
41
  # Declares a composite (computed) attribute.
@@ -97,14 +103,14 @@ module Castkit
97
103
  #
98
104
  # @return [Hash{Symbol => Castkit::Attribute}]
99
105
  def attributes
100
- @attributes ||= {}
106
+ cattri_variable_memoize(:__castkit_attributes_registry) { {} }
101
107
  end
102
108
 
103
109
  def inherited(subclass)
104
110
  super
105
111
 
106
- parent_attributes = instance_variable_get(:@attributes)
107
- subclass.instance_variable_set(:@attributes, parent_attributes.dup) if parent_attributes
112
+ parent_attributes = cattri_variable_get(:__castkit_attributes_registry)
113
+ subclass.cattri_variable_set(:__castkit_attributes_registry, parent_attributes.dup) if parent_attributes
108
114
  end
109
115
 
110
116
  # Alias for {#attribute}
@@ -152,31 +158,27 @@ module Castkit
152
158
  attribute = Castkit::Attribute.new(field, type, **options)
153
159
  attributes[field] = attribute
154
160
 
155
- if attribute.full_access?
156
- attr_reader field
157
-
158
- define_typed_writer(field, attribute)
159
- elsif attribute.writeable?
160
- define_typed_writer(field, attribute)
161
- elsif attribute.readable?
162
- attr_reader field
163
- end
161
+ define_accessors(attribute)
164
162
  end
165
163
 
166
- # Defines a writer method that enforces type coercion.
164
+ # Creates readers/writers for a defined attribute using Cattri.
167
165
  #
168
- # @param field [Symbol]
169
166
  # @param attribute [Castkit::Attribute]
170
167
  # @return [void]
171
- def define_typed_writer(field, attribute)
172
- define_method("#{field}=") do |value|
173
- deserialized_value = Castkit.type_caster(attribute.type.to_sym).call(
174
- value,
175
- options: attribute.options,
176
- context: attribute.field
177
- )
178
-
179
- instance_variable_set("@#{field}", deserialized_value)
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)
180
182
  end
181
183
  end
182
184
 
@@ -189,6 +191,7 @@ module Castkit
189
191
  def with_access(access, options = {}, &block)
190
192
  @__access_context = access
191
193
  @__block_options = options
194
+
192
195
  instance_eval(&block)
193
196
  ensure
194
197
  @__access_context = nil
@@ -204,6 +207,7 @@ module Castkit
204
207
  def with_required(flag, options = {}, &block)
205
208
  @__required_context = flag
206
209
  @__block_options = options
210
+
207
211
  instance_eval(&block)
208
212
  ensure
209
213
  @__required_context = nil
@@ -222,6 +226,26 @@ module Castkit
222
226
 
223
227
  base.merge(options)
224
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
225
249
  end
226
250
  end
227
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
@@ -76,20 +76,23 @@ module Castkit
76
76
  end
77
77
 
78
78
  # @return [Hash{Symbol => Object}] The raw data provided during instantiation.
79
- attr_reader :__raw
79
+ cattri :__raw, nil, expose: :read
80
80
 
81
81
  # @return [Hash{Symbol => Object}] Undefined attributes provided during instantiation.
82
- attr_reader :unknown_attributes
82
+ cattri :unknown_attributes, nil, expose: :read
83
83
 
84
84
  # Initializes the DTO from a hash of attributes.
85
85
  #
86
86
  # @param data [Hash] raw input hash
87
87
  # @raise [Castkit::DataObjectError] if strict mode is enabled and unknown keys are present
88
88
  def initialize(data = {})
89
- @__raw = data.dup.freeze
89
+ super()
90
+
91
+ cattri_variable_set(:__raw, data.dup.freeze)
90
92
  data = unwrap_root(data)
91
93
 
92
- @unknown_attributes = data.reject { |key, _| self.class.attributes.key?(key.to_sym) }.freeze
94
+ cattri_variable_set(:unknown_attributes,
95
+ data.reject { |key, _| self.class.attributes.key?(key.to_sym) }.freeze)
93
96
 
94
97
  validate_data!(data)
95
98
  deserialize_attributes!(data)
@@ -49,7 +49,7 @@ module Castkit
49
49
  next if value.nil? && attribute.optional?
50
50
 
51
51
  value = deserialize_attribute_value!(attribute, value)
52
- instance_variable_set("@#{attribute.field}", value)
52
+ assign_attribute_value!(attribute, value)
53
53
  end
54
54
  end
55
55
 
@@ -115,6 +115,23 @@ module Castkit
115
115
  nil
116
116
  end
117
117
 
118
+ # Stores a deserialized value using Cattri's internal store when available.
119
+ #
120
+ # @param attribute [Castkit::Attribute]
121
+ # @param value [Object]
122
+ # @return [void]
123
+ def assign_attribute_value!(attribute, value)
124
+ if respond_to?(:cattri_variable_set, true)
125
+ cattri_variable_set(
126
+ attribute.field,
127
+ value,
128
+ final: attribute.options[:final]
129
+ )
130
+ else
131
+ instance_variable_set("@#{attribute.field}", value)
132
+ end
133
+ end
134
+
118
135
  # Resolves root-wrapped and unwrapped data.
119
136
  #
120
137
  # @param data [Hash]
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module DSL
5
+ module DataObject
6
+ # Provides opt-in attribute introspection for data objects using Cattri's registry
7
+ # without overriding Castkit's attribute DSL.
8
+ module Introspection
9
+ # Enables introspection helpers on the including class.
10
+ #
11
+ # @return [void]
12
+ def enable_cattri_introspection!
13
+ extend IntrospectionHelpers
14
+
15
+ @cattri_attribute_registry = nil
16
+ end
17
+
18
+ # Class-level helpers that read from Cattri's attribute registry but do not
19
+ # override Castkit's attribute builder.
20
+ module IntrospectionHelpers
21
+ def attribute_defined?(name)
22
+ !!cattri_attribute(name)
23
+ end
24
+
25
+ def attribute_definitions(with_ancestors: false)
26
+ cattri_attribute_registry.defined_attributes(with_ancestors: with_ancestors)
27
+ end
28
+
29
+ def attribute_methods
30
+ cattri_attribute_registry.defined_attributes(with_ancestors: true).transform_values do |attribute|
31
+ Set.new(attribute.allowed_methods)
32
+ end
33
+ end
34
+
35
+ def attribute_source(name)
36
+ cattri_attribute(name)&.defined_in
37
+ end
38
+
39
+ private
40
+
41
+ def cattri_attribute_registry
42
+ @cattri_attribute_registry ||= attribute_registry
43
+ end
44
+
45
+ def cattri_attribute(name)
46
+ cattri_attribute_registry.defined_attributes(with_ancestors: true)[name.to_sym]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -38,7 +38,10 @@ module Castkit
38
38
  # @param value [Boolean, nil]
39
39
  # @return [Boolean]
40
40
  def ignore_blank(value = nil)
41
- @ignore_blank = value.nil? || value
41
+ return (@ignore_blank = true) if value.nil? && !defined?(@ignore_blank)
42
+ return @ignore_blank if value.nil?
43
+
44
+ @ignore_blank = value
42
45
  end
43
46
  end
44
47
 
@@ -7,6 +7,7 @@ require_relative "data_object/contract"
7
7
  require_relative "data_object/plugins"
8
8
  require_relative "data_object/serialization"
9
9
  require_relative "data_object/deserialization"
10
+ require_relative "data_object/introspection"
10
11
 
11
12
  module Castkit
12
13
  module DSL
@@ -47,11 +48,14 @@ module Castkit
47
48
  # @param base [Class] the including class
48
49
  # @return [void]
49
50
  def self.included(base)
51
+ base.include(Cattri)
52
+
50
53
  base.extend(Castkit::Core::Config)
51
54
  base.extend(Castkit::Core::Attributes)
52
55
  base.extend(Castkit::Core::AttributeTypes)
53
56
  base.extend(Castkit::DSL::DataObject::Contract)
54
57
  base.extend(Castkit::DSL::DataObject::Plugins)
58
+ base.extend(Castkit::DSL::DataObject::Introspection)
55
59
 
56
60
  base.include(Castkit::DSL::DataObject::Serialization)
57
61
  base.include(Castkit::DSL::DataObject::Deserialization)
data/lib/castkit/error.rb CHANGED
@@ -3,8 +3,10 @@
3
3
  module Castkit
4
4
  # Base error class for all Castkit-related exceptions.
5
5
  class Error < StandardError
6
+ include Cattri
7
+
6
8
  # @return [Hash, Object, nil] contextual data to aid in debugging
7
- attr_reader :context
9
+ cattri :context, nil, expose: :read
8
10
 
9
11
  # Initializes a Castkit error.
10
12
  #
@@ -12,7 +14,8 @@ module Castkit
12
14
  # @param context [Object, String, nil] optional data object or hash for context
13
15
  def initialize(msg, context: nil)
14
16
  super(msg)
15
- @context = context
17
+
18
+ cattri_variable_set(:context, context, final: true)
16
19
  end
17
20
  end
18
21
 
@@ -44,11 +47,12 @@ module Castkit
44
47
 
45
48
  # Raised during contract validation.
46
49
  class ContractError < Error
47
- attr_reader :errors
50
+ cattri :errors, {}, expose: :read
48
51
 
49
52
  def initialize(msg, context: nil, errors: nil)
50
53
  super(msg, context: context)
51
- @errors = errors || {}
54
+
55
+ cattri_variable_set(:errors, errors || {})
52
56
  end
53
57
  end
54
58
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cattri"
4
+
3
5
  module Castkit
4
6
  # Internal registry for Castkit plugin modules.
5
7
  #
@@ -19,9 +21,16 @@ module Castkit
19
21
  # enable_plugins :custom, :oj
20
22
  # end
21
23
  module Plugins
22
- @registered_plugins = {}
24
+ include Cattri
23
25
 
24
26
  class << self
27
+ include Cattri
28
+ extend Cattri::Dsl
29
+ extend Cattri::ClassMethods
30
+ extend Cattri::Visibility
31
+
32
+ cattri :registered_plugins, {}, expose: :read_write
33
+
25
34
  # Activates one or more plugins on the given class.
26
35
  #
27
36
  # Each plugin module is included into the class. If the module responds to `setup!`,
@@ -58,7 +67,7 @@ module Castkit
58
67
  # @return [Module] the plugin module
59
68
  # @raise [Castkit::Error] if no plugin is found
60
69
  def lookup!(name)
61
- @registered_plugins[name.to_sym] ||
70
+ registered_plugins[name.to_sym] ||
62
71
  const_get(Castkit::Inflector.pascalize(name.to_s), false)
63
72
  rescue NameError
64
73
  raise Castkit::Error,
@@ -75,7 +84,7 @@ module Castkit
75
84
  # @param plugin [Module] the plugin module to register
76
85
  # @return [void]
77
86
  def register(name, plugin)
78
- @registered_plugins[name] = plugin
87
+ registered_plugins[name.to_sym] = plugin
79
88
  end
80
89
  end
81
90
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "set"
4
+ require "cattri"
4
5
 
5
6
  module Castkit
6
7
  module Serializers
@@ -21,6 +22,8 @@ module Castkit
21
22
  #
22
23
  # CustomSerializer.call(user_dto)
23
24
  class Base
25
+ include Cattri
26
+
24
27
  class << self
25
28
  # Entrypoint for serializing an object.
26
29
  #
@@ -33,7 +36,7 @@ module Castkit
33
36
  end
34
37
 
35
38
  # @return [Castkit::DataObject] the object being serialized
36
- attr_reader :object
39
+ cattri :object, nil, expose: :read
37
40
 
38
41
  protected
39
42
 
@@ -47,15 +50,17 @@ module Castkit
47
50
  private
48
51
 
49
52
  # @return [Set<Integer>] a set of visited object IDs to detect circular references
50
- attr_reader :visited
53
+ cattri :visited, nil, expose: :read
51
54
 
52
55
  # Initializes the serializer instance.
53
56
  #
54
57
  # @param object [Castkit::DataObject]
55
58
  # @param visited [Set, nil]
56
59
  def initialize(object, visited: nil)
57
- @object = object
58
- @visited = visited || Set.new
60
+ super()
61
+
62
+ cattri_variable_set(:object, object)
63
+ cattri_variable_set(:visited, visited || Set.new)
59
64
  end
60
65
 
61
66
  # Subclasses must override this method to implement serialization logic.
@@ -11,13 +11,13 @@ module Castkit
11
11
  # and respects the class-level serialization configuration.
12
12
  class DefaultSerializer < Castkit::Serializers::Base
13
13
  # @return [Hash{Symbol => Castkit::Attribute}] the attributes to serialize
14
- attr_reader :attributes
14
+ cattri :attributes, nil, expose: :read
15
15
 
16
16
  # @return [Hash{Symbol => Object}] unrecognized attributes captured during deserialization
17
- attr_reader :unknown_attributes
17
+ cattri :unknown_attributes, nil, expose: :read
18
18
 
19
19
  # @return [Hash] serialization config flags like :root, :ignore_nil, :allow_unknown
20
- attr_reader :options
20
+ cattri :options, nil, expose: :read
21
21
 
22
22
  # Serializes the object to a hash.
23
23
  #
@@ -41,13 +41,13 @@ module Castkit
41
41
  super
42
42
 
43
43
  @skip_flag = "__castkit_#{object.object_id}"
44
- @attributes = object.class.attributes.freeze
45
- @unknown_attributes = object.unknown_attributes.freeze
46
- @options = {
47
- root: object.class.root,
48
- ignore_nil: object.class.ignore_nil || false,
49
- allow_unknown: object.class.allow_unknown || false
50
- }
44
+ cattri_variable_set(:attributes, object.class.attributes.freeze)
45
+ cattri_variable_set(:unknown_attributes, object.unknown_attributes.freeze)
46
+ cattri_variable_set(:options, {
47
+ root: object.class.root,
48
+ ignore_nil: object.class.ignore_nil || false,
49
+ allow_unknown: object.class.allow_unknown || false
50
+ })
51
51
  end
52
52
 
53
53
  # Serializes all defined attributes.
@@ -17,11 +17,11 @@ module Castkit
17
17
  # Validates that the value is an Array.
18
18
  #
19
19
  # @param value [Object] the value to validate
20
- # @param _options [Hash] unused, for interface consistency
20
+ # @param options [Hash] unused, for interface consistency
21
21
  # @param context [Symbol, String] the field or context for error messaging
22
22
  # @return [void]
23
23
  # @raise [Castkit::AttributeError] if value is not an Array
24
- def call(value, _options:, context:)
24
+ def call(value, options:, context:) # rubocop:disable Lint/UnusedMethodArgument
25
25
  type_error!(:array, value, context: context) unless value.is_a?(::Array)
26
26
  end
27
27
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: castkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Lucas
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-09 00:00:00.000000000 Z
11
+ date: 2025-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cattri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.3
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: thor
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov-html
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: yard
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -138,6 +166,7 @@ files:
138
166
  - lib/castkit/dsl/data_object.rb
139
167
  - lib/castkit/dsl/data_object/contract.rb
140
168
  - lib/castkit/dsl/data_object/deserialization.rb
169
+ - lib/castkit/dsl/data_object/introspection.rb
141
170
  - lib/castkit/dsl/data_object/plugins.rb
142
171
  - lib/castkit/dsl/data_object/serialization.rb
143
172
  - lib/castkit/error.rb