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
@@ -5,14 +5,7 @@ require_relative "error"
5
5
  require_relative "attribute"
6
6
  require_relative "serializers/default_serializer"
7
7
  require_relative "contract/validator"
8
- require_relative "core/config"
9
- require_relative "core/attributes"
10
- require_relative "core/attribute_types"
11
- require_relative "core/registerable"
12
- require_relative "ext/data_object/contract"
13
- require_relative "ext/data_object/deserialization"
14
- require_relative "ext/data_object/plugins"
15
- require_relative "ext/data_object/serialization"
8
+ require_relative "dsl/data_object"
16
9
 
17
10
  module Castkit
18
11
  # Base class for defining declarative, typed data transfer objects (DTOs).
@@ -30,25 +23,9 @@ module Castkit
30
23
  # user = UserDto.new(name: "Alice", age: 30)
31
24
  # user.to_json #=> '{"name":"Alice","age":30}'
32
25
  class DataObject
33
- extend Castkit::Core::Config
34
- extend Castkit::Core::Attributes
35
- extend Castkit::Core::AttributeTypes
36
- extend Castkit::Core::Registerable
37
- extend Castkit::Ext::DataObject::Contract
38
- extend Castkit::Ext::DataObject::Plugins
39
-
40
- include Castkit::Ext::DataObject::Serialization
41
- include Castkit::Ext::DataObject::Deserialization
26
+ include Castkit::DSL::DataObject
42
27
 
43
28
  class << self
44
- # Registers the current class under `Castkit::DataObjects`.
45
- #
46
- # @param as [String, Symbol, nil] The constant name to use (PascalCase). Defaults to class name or "Anonymous".
47
- # @return [Class] the registered dataobject class
48
- def register!(as: nil)
49
- super(namespace: :dataobjects, as: as)
50
- end
51
-
52
29
  def build(&block)
53
30
  klass = Class.new(self)
54
31
  klass.class_eval(&block) if block_given?
@@ -99,20 +76,23 @@ module Castkit
99
76
  end
100
77
 
101
78
  # @return [Hash{Symbol => Object}] The raw data provided during instantiation.
102
- attr_reader :__raw
79
+ cattri :__raw, nil, expose: :read
103
80
 
104
81
  # @return [Hash{Symbol => Object}] Undefined attributes provided during instantiation.
105
- attr_reader :unknown_attributes
82
+ cattri :unknown_attributes, nil, expose: :read
106
83
 
107
84
  # Initializes the DTO from a hash of attributes.
108
85
  #
109
86
  # @param data [Hash] raw input hash
110
87
  # @raise [Castkit::DataObjectError] if strict mode is enabled and unknown keys are present
111
88
  def initialize(data = {})
112
- @__raw = data.dup.freeze
89
+ super()
90
+
91
+ cattri_variable_set(:__raw, data.dup.freeze)
113
92
  data = unwrap_root(data)
114
93
 
115
- @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)
116
96
 
117
97
  validate_data!(data)
118
98
  deserialize_attributes!(data)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module Attribute
6
6
  # Provides access control helpers for attributes.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module Attribute
6
6
  # Provides centralized handling of attribute casting and validation errors.
7
7
  #
@@ -3,7 +3,7 @@
3
3
  require_relative "../../data_object"
4
4
 
5
5
  module Castkit
6
- module Ext
6
+ module DSL
7
7
  module Attribute
8
8
  # Provides access to normalized attribute options and helper predicates.
9
9
  #
@@ -4,13 +4,13 @@ require_relative "error_handling"
4
4
  require_relative "options"
5
5
 
6
6
  module Castkit
7
- module Ext
7
+ module DSL
8
8
  module Attribute
9
9
  # Provides validation logic for attribute configuration.
10
10
  #
11
11
  # These checks are typically performed at attribute initialization to catch misconfigurations early.
12
12
  module Validation
13
- include Castkit::Ext::Attribute::ErrorHandling
13
+ include Castkit::DSL::Attribute::ErrorHandling
14
14
 
15
15
  private
16
16
 
@@ -58,7 +58,7 @@ module Castkit
58
58
  # @raise [Castkit::AttributeError] if any access mode is invalid and enforcement is enabled
59
59
  def validate_access!
60
60
  access.each do |mode|
61
- next if Castkit::Ext::Attribute::Options::DEFAULT_OPTIONS[:access].include?(mode)
61
+ next if Castkit::Attributes::Options::DEFAULTS[:access].include?(mode)
62
62
 
63
63
  handle_error(:access, mode: mode, context: to_h)
64
64
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute/options"
4
+ require_relative "attribute/access"
5
+ require_relative "attribute/validation"
6
+
7
+ module Castkit
8
+ module DSL
9
+ # Provides a unified entry point for attribute-level DSL extensions.
10
+ #
11
+ # This module bundles together the core DSL modules for configuring attributes.
12
+ # It is included internally by systems that support Castkit-style attribute declarations,
13
+ # such as {Castkit::DataObject} and {Castkit::Contract::Base}.
14
+ #
15
+ # When included, it mixes in:
16
+ # - {Castkit::DSL::Attribute::Options} – option-setting methods (e.g., `required`, `default`, etc.)
17
+ # - {Castkit::DSL::Attribute::Access} – access control methods (e.g., `readonly`, `access`)
18
+ # - {Castkit::DSL::Attribute::Validation} – validation helpers (e.g., `format`, `validator`)
19
+ #
20
+ # @example Extending a custom DSL that uses Castkit-style attributes
21
+ # class MyCustomSchema
22
+ # include Castkit::DSL::Attribute
23
+ #
24
+ # def self.required(value)
25
+ # # interpret DSL options
26
+ # end
27
+ # end
28
+ #
29
+ # class MyString < MyCustomSchema
30
+ # type :string
31
+ # required true
32
+ # access [:read]
33
+ # end
34
+ #
35
+ # @note This module is not intended to be mixed into {Castkit::Attributes::Definition}.
36
+ module Attribute
37
+ # Hook called when this module is included.
38
+ #
39
+ # @param base [Class, Module] the including class or module
40
+ def self.included(base)
41
+ base.include(Castkit::DSL::Attribute::Options)
42
+ base.include(Castkit::DSL::Attribute::Access)
43
+ base.include(Castkit::DSL::Attribute::Validation)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -3,7 +3,7 @@
3
3
  require_relative "../../contract"
4
4
 
5
5
  module Castkit
6
- module Ext
6
+ module DSL
7
7
  module DataObject
8
8
  # Extension module that adds contract support to Castkit::DataObject classes.
9
9
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module DataObject
6
6
  # Adds deserialization support for Castkit::DataObject instances.
7
7
  #
@@ -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
 
@@ -104,13 +104,34 @@ module Castkit
104
104
  # @return [Object, nil]
105
105
  def resolve_input_value(input, attribute)
106
106
  attribute.key_path(with_aliases: true).each do |path|
107
- value = path.reduce(input) { |memo, key| memo.is_a?(Hash) ? memo[key] : nil }
107
+ value = path.reduce(input) do |memo, key|
108
+ next memo unless memo.is_a?(Hash)
109
+
110
+ memo.key?(key) ? memo[key] : memo[key.to_s]
111
+ end
108
112
  return value unless value.nil?
109
113
  end
110
114
 
111
115
  nil
112
116
  end
113
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
+
114
135
  # Resolves root-wrapped and unwrapped data.
115
136
  #
116
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module DataObject
6
6
  # Provides plugin support for DataObject classes.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- module Ext
4
+ module DSL
5
5
  module DataObject
6
6
  # Provides per-class serialization configuration for Castkit::Dataobject, including
7
7
  # root key handling and ignore rules.
@@ -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
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/config"
4
+ require_relative "../core/attributes"
5
+ require_relative "../core/attribute_types"
6
+ require_relative "data_object/contract"
7
+ require_relative "data_object/plugins"
8
+ require_relative "data_object/serialization"
9
+ require_relative "data_object/deserialization"
10
+ require_relative "data_object/introspection"
11
+
12
+ module Castkit
13
+ module DSL
14
+ # Provides the complete DSL used by Castkit data objects.
15
+ #
16
+ # This module can be included into any class to make it behave like a `Castkit::DataObject`
17
+ # without requiring subclassing. It wires in the full attribute DSL, type system, contract support,
18
+ # plugin lifecycle, and (de)serialization logic.
19
+ #
20
+ # This is what powers `Castkit::DataObject` internally, and is intended for advanced use
21
+ # cases where composition is preferred over inheritance.
22
+ #
23
+ # When included, this module:
24
+ #
25
+ # - `extend`s:
26
+ # - {Castkit::Core::Config} – configuration and context behavior
27
+ # - {Castkit::Core::Attributes} – the DSL for declaring attributes
28
+ # - {Castkit::Core::AttributeTypes} – support for custom type resolution
29
+ # - {Castkit::DSL::DataObject::Contract} – validation contract hooks
30
+ # - {Castkit::DSL::DataObject::Plugins} – plugin hooks and lifecycle events
31
+ #
32
+ # - `include`s:
33
+ # - {Castkit::DSL::DataObject::Serialization} – `#to_h`, `#as_json`, etc.
34
+ # - {Castkit::DSL::DataObject::Deserialization} – `from_h`, `from_json`, etc.
35
+ #
36
+ # @example Including in a custom data object
37
+ # class MyObject
38
+ # include Castkit::DSL::DataObject
39
+ #
40
+ # string :id
41
+ # boolean :active, default: true
42
+ # end
43
+ #
44
+ # @see Castkit::DataObject for the default implementation
45
+ module DataObject
46
+ # Hook triggered when the module is included.
47
+ #
48
+ # @param base [Class] the including class
49
+ # @return [void]
50
+ def self.included(base)
51
+ base.include(Cattri)
52
+
53
+ base.extend(Castkit::Core::Config)
54
+ base.extend(Castkit::Core::Attributes)
55
+ base.extend(Castkit::Core::AttributeTypes)
56
+ base.extend(Castkit::DSL::DataObject::Contract)
57
+ base.extend(Castkit::DSL::DataObject::Plugins)
58
+ base.extend(Castkit::DSL::DataObject::Introspection)
59
+
60
+ base.include(Castkit::DSL::DataObject::Serialization)
61
+ base.include(Castkit::DSL::DataObject::Deserialization)
62
+ end
63
+ end
64
+ end
65
+ end
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.
@@ -24,18 +24,19 @@ module Castkit
24
24
  # @param options [Hash] options passed to `validate!`, e.g., `min`, `max`, `force_type`
25
25
  # @param context [Symbol, String, nil] context label for error messages
26
26
  # @return [Object] the deserialized and validated value
27
- def cast!(value, validator: nil, options: {}, context: {})
27
+ def cast!(value, validator: nil, options: {}, context: {}, **extra_options)
28
+ options = options.merge(extra_options)
28
29
  instance = new
29
30
  validator ||= options.delete(:validator)
30
31
  validator ||= default_validator(instance)
31
32
 
32
33
  if options[:force_type]
33
34
  deserialized_value = instance.deserialize(value)
34
- validator.call(deserialized_value, options: options, context: context)
35
+ invoke_validator(validator, deserialized_value, options: options, context: context)
35
36
  return deserialized_value
36
37
  end
37
38
 
38
- validator.call(value, options: options, context: context)
39
+ invoke_validator(validator, value, options: options, context: context)
39
40
  instance.deserialize(value)
40
41
  end
41
42
 
@@ -76,6 +77,26 @@ module Castkit
76
77
  instance.validate!(value, options: options, context: context)
77
78
  end
78
79
  end
80
+
81
+ # Dispatches validation to support callable validators with different arities.
82
+ #
83
+ # @param validator [#call, Proc] the validator to invoke
84
+ # @param value [Object] the value being validated
85
+ # @param options [Hash] validation options
86
+ # @param context [Symbol, String, nil] context for error messages
87
+ # @return [void]
88
+ def invoke_validator(validator, value, options:, context:)
89
+ return validator.call(value, options: options, context: context) unless validator.is_a?(Proc)
90
+
91
+ case validator.arity
92
+ when 1
93
+ validator.call(value)
94
+ when 2
95
+ validator.call(value, options)
96
+ else
97
+ validator.call(value, options: options, context: context)
98
+ end
99
+ end
79
100
  end
80
101
 
81
102
  # Deserializes the value. Override in subclasses to coerce input (e.g., `"123"` → `123`).
@@ -16,15 +16,15 @@ module Castkit
16
16
  # validator.call("true", _options: {}, context: :enabled) # => true
17
17
  # validator.call("0", _options: {}, context: :enabled) # => false
18
18
  # validator.call("nope", _options: {}, context: :enabled) # raises Castkit::AttributeError
19
- class BooleanValidator
19
+ class BooleanValidator < Castkit::Validators::Base
20
20
  # Validates the Boolean value.
21
21
  #
22
22
  # @param value [Object] the input to validate
23
- # @param _options [Hash] unused, provided for consistency with other validators
23
+ # @param options [Hash] unused, provided for consistency with other validators
24
24
  # @param context [Symbol, String] the attribute name or path for error messages
25
25
  # @return [Boolean]
26
26
  # @raise [Castkit::AttributeError] if the value is not a recognizable boolean
27
- def call(value, _options:, context:)
27
+ def call(value, options:, context:) # rubocop:disable Lint/UnusedMethodArgument
28
28
  case value.to_s.downcase
29
29
  when "true", "1"
30
30
  true
@@ -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.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/castkit.rb CHANGED
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "castkit/version"
4
- require_relative "castkit/attribute"
5
- require_relative "castkit/contract"
6
- require_relative "castkit/data_object"
3
+ require_relative "castkit/castkit"
7
4
 
8
5
  # Castkit is a lightweight, type-safe data object system for Ruby.
9
6
  #