castkit 0.2.0 → 0.3.1

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/README.md +297 -13
  4. data/castkit.gemspec +3 -0
  5. data/lib/castkit/attribute.rb +82 -59
  6. data/lib/castkit/attributes/definition.rb +64 -0
  7. data/lib/castkit/attributes/options.rb +214 -0
  8. data/lib/castkit/castkit.rb +18 -5
  9. data/lib/castkit/cli/generate.rb +112 -0
  10. data/lib/castkit/cli/list.rb +200 -0
  11. data/lib/castkit/cli/main.rb +43 -0
  12. data/lib/castkit/cli.rb +24 -0
  13. data/lib/castkit/configuration.rb +31 -8
  14. data/lib/castkit/contract/{generic.rb → base.rb} +5 -17
  15. data/lib/castkit/contract/result.rb +2 -2
  16. data/lib/castkit/contract/validator.rb +5 -1
  17. data/lib/castkit/contract.rb +5 -5
  18. data/lib/castkit/core/attributes.rb +87 -44
  19. data/lib/castkit/data_object.rb +11 -30
  20. data/lib/castkit/{ext → dsl}/attribute/access.rb +1 -1
  21. data/lib/castkit/{ext → dsl}/attribute/error_handling.rb +1 -1
  22. data/lib/castkit/{ext → dsl}/attribute/options.rb +1 -1
  23. data/lib/castkit/{ext → dsl}/attribute/validation.rb +3 -3
  24. data/lib/castkit/dsl/attribute.rb +47 -0
  25. data/lib/castkit/{ext → dsl}/data_object/contract.rb +2 -2
  26. data/lib/castkit/{ext → dsl}/data_object/deserialization.rb +6 -2
  27. data/lib/castkit/dsl/data_object/plugins.rb +86 -0
  28. data/lib/castkit/{ext → dsl}/data_object/serialization.rb +1 -1
  29. data/lib/castkit/dsl/data_object.rb +61 -0
  30. data/lib/castkit/inflector.rb +1 -1
  31. data/lib/castkit/plugins.rb +82 -0
  32. data/lib/castkit/serializers/base.rb +94 -0
  33. data/lib/castkit/serializers/default_serializer.rb +156 -0
  34. data/lib/castkit/types/{generic.rb → base.rb} +30 -10
  35. data/lib/castkit/types/boolean.rb +14 -10
  36. data/lib/castkit/types/collection.rb +13 -2
  37. data/lib/castkit/types/date.rb +2 -2
  38. data/lib/castkit/types/date_time.rb +2 -2
  39. data/lib/castkit/types/float.rb +5 -5
  40. data/lib/castkit/types/integer.rb +5 -5
  41. data/lib/castkit/types/string.rb +2 -2
  42. data/lib/castkit/types.rb +1 -1
  43. data/lib/castkit/validators/base.rb +59 -0
  44. data/lib/castkit/validators/boolean_validator.rb +39 -0
  45. data/lib/castkit/validators/collection_validator.rb +29 -0
  46. data/lib/castkit/validators/float_validator.rb +31 -0
  47. data/lib/castkit/validators/integer_validator.rb +31 -0
  48. data/lib/castkit/validators/numeric_validator.rb +2 -2
  49. data/lib/castkit/validators/string_validator.rb +3 -4
  50. data/lib/castkit/version.rb +1 -1
  51. data/lib/castkit.rb +1 -4
  52. data/lib/generators/attribute.rb +39 -0
  53. data/lib/generators/base.rb +97 -0
  54. data/lib/generators/contract.rb +68 -0
  55. data/lib/generators/data_object.rb +48 -0
  56. data/lib/generators/plugin.rb +25 -0
  57. data/lib/generators/serializer.rb +28 -0
  58. data/lib/generators/templates/attribute.rb.tt +21 -0
  59. data/lib/generators/templates/attribute_spec.rb.tt +41 -0
  60. data/lib/generators/templates/contract.rb.tt +26 -0
  61. data/lib/generators/templates/contract_spec.rb.tt +76 -0
  62. data/lib/generators/templates/data_object.rb.tt +17 -0
  63. data/lib/generators/templates/data_object_spec.rb.tt +36 -0
  64. data/lib/generators/templates/plugin.rb.tt +37 -0
  65. data/lib/generators/templates/plugin_spec.rb.tt +18 -0
  66. data/lib/generators/templates/serializer.rb.tt +24 -0
  67. data/lib/generators/templates/serializer_spec.rb.tt +14 -0
  68. data/lib/generators/templates/type.rb.tt +57 -0
  69. data/lib/generators/templates/type_spec.rb.tt +42 -0
  70. data/lib/generators/templates/validator.rb.tt +26 -0
  71. data/lib/generators/templates/validator_spec.rb.tt +23 -0
  72. data/lib/generators/type.rb +29 -0
  73. data/lib/generators/validator.rb +41 -0
  74. metadata +92 -16
  75. data/.rspec_status +0 -196
  76. data/lib/castkit/core/registerable.rb +0 -59
  77. data/lib/castkit/default_serializer.rb +0 -154
  78. data/lib/castkit/serializer.rb +0 -92
  79. data/lib/castkit/validators/base_validator.rb +0 -39
@@ -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
  #
@@ -63,7 +63,7 @@ module Castkit
63
63
  # UserDto = Castkit::DataObject.from_contract(UserContract)
64
64
  # dto = UserDto.new(id: "abc", email: "a@example.com")
65
65
  #
66
- # @param contract [Class<Castkit::Contract::Generic>] the contract to convert
66
+ # @param contract [Class<Castkit::Contract::Base>] the contract to convert
67
67
  # @return [Class<Castkit::DataObject>] a new anonymous DataObject class
68
68
 
69
69
  def from_contract(contract)
@@ -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
  #
@@ -104,7 +104,11 @@ 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
 
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module DSL
5
+ module DataObject
6
+ # Provides plugin support for DataObject classes.
7
+ #
8
+ # This module allows a Castkit::DataObject to explicitly declare supported plugins,
9
+ # and ensures all default plugins are enabled on subclassing.
10
+ #
11
+ # Plugins should be defined under `Castkit::Plugins::<Name>` and can be registered
12
+ # globally via `Castkit.configure { |c| c.register_plugin(:name, MyPlugin) }`.
13
+ #
14
+ # Example:
15
+ # class MyDto < Castkit::DataObject
16
+ # enable_plugins :oj, :msgpack
17
+ # disable_plugins :yaml
18
+ # end
19
+ #
20
+ module Plugins
21
+ # Returns the set of plugins explicitly enabled on the class.
22
+ #
23
+ # @return [Set<Symbol>] enabled plugin names
24
+ def enabled_plugins
25
+ @enabled_plugins ||= Set.new
26
+ end
27
+
28
+ # Returns the set of default plugins explicitly disabled on the class.
29
+ #
30
+ # @return [Set<Symbol>] disabled plugin names
31
+ def disabled_plugins
32
+ @disabled_plugins ||= Set.new
33
+ end
34
+
35
+ # Enables one or more plugins on the calling class.
36
+ #
37
+ # @param plugins [Array<Symbol>] plugin identifiers (e.g., :oj, :yaml)
38
+ # @return [void]
39
+ def enable_plugins(*plugins)
40
+ return if plugins.empty?
41
+
42
+ @enabled_plugins ||= Set.new
43
+ @enabled_plugins.merge(plugins)
44
+
45
+ Castkit::Plugins.activate(self, *plugins)
46
+ end
47
+
48
+ # Disables one or more default plugins on the calling class.
49
+ #
50
+ # @example
51
+ # Castkit.configure do |config|
52
+ # config.default_plugins [:oj, :activerecord]
53
+ # end
54
+ #
55
+ # class UserDto < Castkit::DataObject
56
+ # disable_plugin :activerecord
57
+ # end
58
+ #
59
+ # @param plugins [Array<Symbol>] plugin identifiers (e.g., :oj, :yaml)
60
+ # @return [void]
61
+ def disable_plugins(*plugins)
62
+ return if plugins.empty?
63
+
64
+ @disabled_plugins ||= Set.new
65
+ @disabled_plugins.merge(plugins)
66
+ end
67
+
68
+ # Hook that is called when a DataObject subclass is created.
69
+ #
70
+ # Automatically applies `Castkit.configuration.default_plugins`
71
+ # to the subclass.
72
+ #
73
+ # @param subclass [Class] the inheriting subclass
74
+ # @return [void]
75
+ def inherited(subclass)
76
+ super
77
+
78
+ disabled = instance_variable_get(:@disabled_plugins) || Set.new
79
+ plugins = Castkit.configuration.default_plugins - disabled.to_a
80
+
81
+ subclass.enable_plugins(*plugins)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ 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 per-class serialization configuration for Castkit::Dataobject, including
7
7
  # root key handling and ignore rules.
@@ -0,0 +1,61 @@
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
+
11
+ module Castkit
12
+ module DSL
13
+ # Provides the complete DSL used by Castkit data objects.
14
+ #
15
+ # This module can be included into any class to make it behave like a `Castkit::DataObject`
16
+ # without requiring subclassing. It wires in the full attribute DSL, type system, contract support,
17
+ # plugin lifecycle, and (de)serialization logic.
18
+ #
19
+ # This is what powers `Castkit::DataObject` internally, and is intended for advanced use
20
+ # cases where composition is preferred over inheritance.
21
+ #
22
+ # When included, this module:
23
+ #
24
+ # - `extend`s:
25
+ # - {Castkit::Core::Config} – configuration and context behavior
26
+ # - {Castkit::Core::Attributes} – the DSL for declaring attributes
27
+ # - {Castkit::Core::AttributeTypes} – support for custom type resolution
28
+ # - {Castkit::DSL::DataObject::Contract} – validation contract hooks
29
+ # - {Castkit::DSL::DataObject::Plugins} – plugin hooks and lifecycle events
30
+ #
31
+ # - `include`s:
32
+ # - {Castkit::DSL::DataObject::Serialization} – `#to_h`, `#as_json`, etc.
33
+ # - {Castkit::DSL::DataObject::Deserialization} – `from_h`, `from_json`, etc.
34
+ #
35
+ # @example Including in a custom data object
36
+ # class MyObject
37
+ # include Castkit::DSL::DataObject
38
+ #
39
+ # string :id
40
+ # boolean :active, default: true
41
+ # end
42
+ #
43
+ # @see Castkit::DataObject for the default implementation
44
+ module DataObject
45
+ # Hook triggered when the module is included.
46
+ #
47
+ # @param base [Class] the including class
48
+ # @return [void]
49
+ def self.included(base)
50
+ base.extend(Castkit::Core::Config)
51
+ base.extend(Castkit::Core::Attributes)
52
+ base.extend(Castkit::Core::AttributeTypes)
53
+ base.extend(Castkit::DSL::DataObject::Contract)
54
+ base.extend(Castkit::DSL::DataObject::Plugins)
55
+
56
+ base.include(Castkit::DSL::DataObject::Serialization)
57
+ base.include(Castkit::DSL::DataObject::Deserialization)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -24,7 +24,7 @@ module Castkit
24
24
  # @param string [String, Symbol] the input to convert
25
25
  # @return [String] the PascalCase representation
26
26
  def pascalize(string)
27
- string.to_s.split("_").map(&:capitalize).join
27
+ underscore(string).to_s.split("_").map(&:capitalize).join
28
28
  end
29
29
 
30
30
  # Converts a PascalCase or camelCase string to snake_case.
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ # Internal registry for Castkit plugin modules.
5
+ #
6
+ # This module supports registering, activating, and looking up Castkit plugins.
7
+ # Plugins are typically included in Castkit::DataObject subclasses via `.enable_plugins`.
8
+ #
9
+ # Plugins can be defined under `Castkit::Plugins::<Name>` or manually registered
10
+ # through the configuration API.
11
+ #
12
+ # @example Registering a custom plugin
13
+ # Castkit.configure do |config|
14
+ # config.register_plugin(:custom, MyCustomPlugin)
15
+ # end
16
+ #
17
+ # @example Enabling plugins on a DataObject
18
+ # class MyDto < Castkit::DataObject
19
+ # enable_plugins :custom, :oj
20
+ # end
21
+ module Plugins
22
+ @registered_plugins = {}
23
+
24
+ class << self
25
+ # Activates one or more plugins on the given class.
26
+ #
27
+ # Each plugin module is included into the class. If the module responds to `setup!`,
28
+ # it will be called with the class as the argument.
29
+ #
30
+ # @param klass [Class] the target class (usually a Castkit::DataObject subclass)
31
+ # @param names [Array<Symbol>] plugin names (e.g., :oj, :yaml)
32
+ # @return [void]
33
+ def activate(klass, *names)
34
+ names.each do |name|
35
+ plugin = lookup!(name)
36
+ klass.include(plugin) if plugin
37
+ plugin.setup!(klass) if plugin.respond_to?(:setup!)
38
+ end
39
+ end
40
+
41
+ # (Placeholder) Deactivates plugins by name.
42
+ #
43
+ # Currently not implemented, included for future API completeness.
44
+ #
45
+ # @param _klass [Class] the class to deactivate plugins on
46
+ # @param names [Array<Symbol>] plugin names to deactivate
47
+ # @return [void]
48
+ def deactivate(_klass, *names)
49
+ @deactivate_plugins = names
50
+ end
51
+
52
+ # Looks up a plugin module by name.
53
+ #
54
+ # This will first check the internal registry, then fall back to
55
+ # resolving a constant under `Castkit::Plugins::<Name>`.
56
+ #
57
+ # @param name [Symbol, String] the plugin name (e.g., :oj)
58
+ # @return [Module] the plugin module
59
+ # @raise [Castkit::Error] if no plugin is found
60
+ def lookup!(name)
61
+ @registered_plugins[name.to_sym] ||
62
+ const_get(Castkit::Inflector.pascalize(name.to_s), false)
63
+ rescue NameError
64
+ raise Castkit::Error,
65
+ "Castkit plugin `#{name}` could not be found. Make sure it is " \
66
+ "defined under Castkit::Plugins or registered using " \
67
+ "`Castkit.configure { |c| c.register_plugin(:#{name}, MyPlugin) }`."
68
+ end
69
+
70
+ # Registers a plugin module under a custom name.
71
+ #
72
+ # This allows developers to register modules not defined under Castkit::Plugins.
73
+ #
74
+ # @param name [Symbol] the plugin name (e.g., :custom_plugin)
75
+ # @param plugin [Module] the plugin module to register
76
+ # @return [void]
77
+ def register(name, plugin)
78
+ @registered_plugins[name] = plugin
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Castkit
6
+ module Serializers
7
+ # Abstract base class for defining custom serializers for Castkit::DataObject instances.
8
+ #
9
+ # Handles circular reference detection and provides a consistent `call` API.
10
+ #
11
+ # Subclasses must implement an instance method `#call` that returns a hash-like representation.
12
+ #
13
+ # @example Usage
14
+ # class CustomSerializer < Castkit::Serializers::Base
15
+ # private
16
+ #
17
+ # def call
18
+ # { type: object.class.name, data: object.to_h }
19
+ # end
20
+ # end
21
+ #
22
+ # CustomSerializer.call(user_dto)
23
+ class Base
24
+ class << self
25
+ # Entrypoint for serializing an object.
26
+ #
27
+ # @param object [Castkit::DataObject] the object to serialize
28
+ # @param visited [Set, nil] used to track visited object IDs
29
+ # @return [Object] result of custom serialization
30
+ def call(object, visited: nil)
31
+ new(object, visited: visited).send(:serialize)
32
+ end
33
+ end
34
+
35
+ # @return [Castkit::DataObject] the object being serialized
36
+ attr_reader :object
37
+
38
+ protected
39
+
40
+ # Fallback to the default serializer.
41
+ #
42
+ # @return [Hash]
43
+ def serialize_with_default
44
+ Castkit::Serializers::DefaultSerializer.call(object, visited: visited)
45
+ end
46
+
47
+ private
48
+
49
+ # @return [Set<Integer>] a set of visited object IDs to detect circular references
50
+ attr_reader :visited
51
+
52
+ # Initializes the serializer instance.
53
+ #
54
+ # @param object [Castkit::DataObject]
55
+ # @param visited [Set, nil]
56
+ def initialize(object, visited: nil)
57
+ @object = object
58
+ @visited = visited || Set.new
59
+ end
60
+
61
+ # Subclasses must override this method to implement serialization logic.
62
+ #
63
+ # @raise [NotImplementedError]
64
+ # @return [Object]
65
+ def call
66
+ raise NotImplementedError, "#{self.class.name} must implement `#call`"
67
+ end
68
+
69
+ # Wraps the actual serialization logic with circular reference detection.
70
+ #
71
+ # @return [Object]
72
+ # @raise [Castkit::SerializationError] if a circular reference is detected
73
+ def serialize
74
+ check_circular_reference!
75
+ visited << object.object_id
76
+
77
+ result = call
78
+ visited.delete(object.object_id)
79
+
80
+ result
81
+ end
82
+
83
+ # Raises if the object has already been visited (circular reference).
84
+ #
85
+ # @raise [Castkit::SerializationError]
86
+ # @return [void]
87
+ def check_circular_reference!
88
+ return unless visited.include?(object.object_id)
89
+
90
+ raise Castkit::SerializationError, "Circular reference detected for #{object.class}"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Castkit
6
+ module Serializers
7
+ # Default serializer for Castkit::DataObject instances.
8
+ #
9
+ # Serializes attributes into a plain Ruby hash, applying access rules, nil/blank filtering,
10
+ # and nested structure handling. The output format supports JSON-compatible structures
11
+ # and respects the class-level serialization configuration.
12
+ class DefaultSerializer < Castkit::Serializers::Base
13
+ # @return [Hash{Symbol => Castkit::Attribute}] the attributes to serialize
14
+ attr_reader :attributes
15
+
16
+ # @return [Hash{Symbol => Object}] unrecognized attributes captured during deserialization
17
+ attr_reader :unknown_attributes
18
+
19
+ # @return [Hash] serialization config flags like :root, :ignore_nil, :allow_unknown
20
+ attr_reader :options
21
+
22
+ # Serializes the object to a hash.
23
+ #
24
+ # Includes unknown attributes if configured, and wraps in a root key if defined.
25
+ #
26
+ # @return [Hash] the fully serialized result
27
+ def call
28
+ result = serialize_attributes
29
+ result.merge!(unknown_attributes) if options[:allow_unknown]
30
+
31
+ options[:root] ? { options[:root].to_sym => result } : result
32
+ end
33
+
34
+ private
35
+
36
+ # Initializes the serializer.
37
+ #
38
+ # @param object [Castkit::DataObject] the object to serialize
39
+ # @param visited [Set, nil] tracks circular references
40
+ def initialize(object, visited: nil)
41
+ super
42
+
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
+ }
51
+ end
52
+
53
+ # Serializes all defined attributes.
54
+ #
55
+ # @return [Hash] serialized attribute key-value pairs
56
+ def serialize_attributes
57
+ attributes.values.each_with_object({}) do |attribute, hash|
58
+ next if attribute.skip_serialization?
59
+
60
+ serialized_value = serialize_attribute(attribute)
61
+ next if serialized_value == @skip_flag
62
+
63
+ assign_attribute_key!(attribute, serialized_value, hash)
64
+ end
65
+ end
66
+
67
+ # Serializes a single attribute.
68
+ #
69
+ # @param attribute [Castkit::Attribute]
70
+ # @return [Object] the serialized value or skip flag
71
+ def serialize_attribute(attribute)
72
+ value = object.public_send(attribute.field)
73
+ return @skip_flag if skip_nil?(attribute, value)
74
+
75
+ serialized_value = process_attribute(attribute, value)
76
+ return @skip_flag if skip_blank?(attribute, serialized_value)
77
+
78
+ serialized_value
79
+ end
80
+
81
+ # Delegates serialization based on type.
82
+ #
83
+ # @param attribute [Castkit::Attribute]
84
+ # @param value [Object]
85
+ # @return [Object]
86
+ def process_attribute(attribute, value)
87
+ if attribute.dataobject?
88
+ serialize_dataobject(attribute, value)
89
+ elsif attribute.dataobject_collection?
90
+ Array(value).map { |v| serialize_dataobject(attribute, v) }
91
+ else
92
+ type = Array(attribute.type).first
93
+ Castkit.type_serializer(type).call(value)
94
+ end
95
+ end
96
+
97
+ # Assigns value into nested hash structure based on key path.
98
+ #
99
+ # @param attribute [Castkit::Attribute]
100
+ # @param value [Object]
101
+ # @param hash [Hash]
102
+ # @return [void]
103
+ def assign_attribute_key!(attribute, value, hash)
104
+ key_path = attribute.key_path
105
+ last = key_path.pop
106
+ current = hash
107
+
108
+ key_path.each do |key|
109
+ current[key] ||= {}
110
+ current = current[key]
111
+ end
112
+
113
+ current[last] = value
114
+ end
115
+
116
+ # Whether to skip serialization for nil values.
117
+ #
118
+ # @param attribute [Castkit::Attribute]
119
+ # @param value [Object]
120
+ # @return [Boolean]
121
+ def skip_nil?(attribute, value)
122
+ value.nil? && (attribute.ignore_nil? || options[:ignore_nil])
123
+ end
124
+
125
+ # Whether to skip serialization for blank values.
126
+ #
127
+ # @param attribute [Castkit::Attribute]
128
+ # @param value [Object]
129
+ # @return [Boolean]
130
+ def skip_blank?(attribute, value)
131
+ blank?(value) && (attribute.ignore_blank? || options[:ignore_blank])
132
+ end
133
+
134
+ # True if value is nil or empty.
135
+ #
136
+ # @param value [Object]
137
+ # @return [Boolean]
138
+ def blank?(value)
139
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
140
+ end
141
+
142
+ # Serializes a DataObject using the proper serializer.
143
+ #
144
+ # @param attribute [Castkit::Attribute]
145
+ # @param value [Castkit::DataObject]
146
+ # @return [Object]
147
+ def serialize_dataobject(attribute, value)
148
+ serializer = attribute.options[:serializer]
149
+ serializer ||= value.class.serializer
150
+ serializer ||= Castkit::Serializers::DefaultSerializer
151
+
152
+ serializer.call(value, visited: visited)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -2,14 +2,14 @@
2
2
 
3
3
  module Castkit
4
4
  module Types
5
- # Generic base class for type definitions in Castkit.
5
+ # Abstract base class for type definitions in Castkit.
6
6
  #
7
7
  # Provides default behavior for (de)serialization, validation, and coercion.
8
8
  # All primitive types should subclass this and override methods as needed.
9
9
  #
10
10
  # The `cast!` method is the primary entry point used by attribute processing
11
11
  # to validate and coerce values in a predictable order.
12
- class Generic
12
+ class Base
13
13
  class << self
14
14
  # Coerces and validates a value for use in a Castkit DataObject.
15
15
  #
@@ -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
 
@@ -69,13 +70,33 @@ module Castkit
69
70
 
70
71
  # Builds a default validator from the instance itself.
71
72
  #
72
- # @param instance [Castkit::Types::Generic]
73
+ # @param instance [Castkit::Types::Base]
73
74
  # @return [Proc] a lambda wrapping `#validate!`
74
75
  def default_validator(instance)
75
76
  lambda do |value, options: {}, context: nil|
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`).
@@ -111,10 +132,9 @@ module Castkit
111
132
  # @param type [Symbol]
112
133
  # @param value [Object, nil]
113
134
  # @return [void]
114
- def type_error!(type, value)
115
- message = "value must be a #{type}, got #{value.inspect}"
116
-
117
- raise Castkit::TypeError, message if Castkit.configuration.raise_type_errors
135
+ def type_error!(type, value, context: nil)
136
+ message = "#{context || "value"} must be a #{type}, got #{value}"
137
+ raise Castkit::AttributeError, message if Castkit.configuration.raise_type_errors
118
138
 
119
139
  Castkit.warning "[Castkit] #{message}" if Castkit.configuration.enable_warnings
120
140
  end