castkit 0.1.2 → 0.3.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 +4 -4
- data/.rspec_status +195 -219
- data/CHANGELOG.md +42 -0
- data/README.md +744 -83
- data/castkit.gemspec +1 -0
- data/lib/castkit/attribute.rb +6 -24
- data/lib/castkit/castkit.rb +61 -10
- data/lib/castkit/cli/generate.rb +98 -0
- data/lib/castkit/cli/list.rb +200 -0
- data/lib/castkit/cli/main.rb +43 -0
- data/lib/castkit/cli.rb +24 -0
- data/lib/castkit/configuration.rb +116 -46
- data/lib/castkit/contract/base.rb +168 -0
- data/lib/castkit/contract/data_object.rb +62 -0
- data/lib/castkit/contract/result.rb +74 -0
- data/lib/castkit/contract/validator.rb +248 -0
- data/lib/castkit/contract.rb +67 -0
- data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
- data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
- data/lib/castkit/core/config.rb +74 -0
- data/lib/castkit/core/registerable.rb +59 -0
- data/lib/castkit/data_object.rb +56 -67
- data/lib/castkit/error.rb +15 -3
- data/lib/castkit/ext/attribute/access.rb +67 -0
- data/lib/castkit/ext/attribute/error_handling.rb +63 -0
- data/lib/castkit/ext/attribute/options.rb +142 -0
- data/lib/castkit/ext/attribute/validation.rb +85 -0
- data/lib/castkit/ext/data_object/contract.rb +96 -0
- data/lib/castkit/ext/data_object/deserialization.rb +167 -0
- data/lib/castkit/ext/data_object/plugins.rb +86 -0
- data/lib/castkit/ext/data_object/serialization.rb +61 -0
- data/lib/castkit/inflector.rb +47 -0
- data/lib/castkit/plugins.rb +82 -0
- data/lib/castkit/serializers/base.rb +94 -0
- data/lib/castkit/serializers/default_serializer.rb +156 -0
- data/lib/castkit/types/base.rb +122 -0
- data/lib/castkit/types/boolean.rb +47 -0
- data/lib/castkit/types/collection.rb +35 -0
- data/lib/castkit/types/date.rb +34 -0
- data/lib/castkit/types/date_time.rb +34 -0
- data/lib/castkit/types/float.rb +46 -0
- data/lib/castkit/types/integer.rb +46 -0
- data/lib/castkit/types/string.rb +44 -0
- data/lib/castkit/types.rb +15 -0
- data/lib/castkit/validators/base.rb +59 -0
- data/lib/castkit/validators/boolean_validator.rb +39 -0
- data/lib/castkit/validators/collection_validator.rb +29 -0
- data/lib/castkit/validators/float_validator.rb +31 -0
- data/lib/castkit/validators/integer_validator.rb +31 -0
- data/lib/castkit/validators/numeric_validator.rb +2 -2
- data/lib/castkit/validators/string_validator.rb +3 -4
- data/lib/castkit/version.rb +1 -1
- data/lib/castkit.rb +2 -0
- data/lib/generators/base.rb +97 -0
- data/lib/generators/contract.rb +68 -0
- data/lib/generators/data_object.rb +48 -0
- data/lib/generators/plugin.rb +25 -0
- data/lib/generators/serializer.rb +28 -0
- data/lib/generators/templates/contract.rb.tt +24 -0
- data/lib/generators/templates/contract_spec.rb.tt +76 -0
- data/lib/generators/templates/data_object.rb.tt +15 -0
- data/lib/generators/templates/data_object_spec.rb.tt +36 -0
- data/lib/generators/templates/plugin.rb.tt +37 -0
- data/lib/generators/templates/plugin_spec.rb.tt +18 -0
- data/lib/generators/templates/serializer.rb.tt +24 -0
- data/lib/generators/templates/serializer_spec.rb.tt +14 -0
- data/lib/generators/templates/type.rb.tt +55 -0
- data/lib/generators/templates/type_spec.rb.tt +42 -0
- data/lib/generators/templates/validator.rb.tt +26 -0
- data/lib/generators/templates/validator_spec.rb.tt +23 -0
- data/lib/generators/type.rb +29 -0
- data/lib/generators/validator.rb +41 -0
- metadata +74 -15
- data/lib/castkit/attribute_extensions/access.rb +0 -65
- data/lib/castkit/attribute_extensions/casting.rb +0 -147
- data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
- data/lib/castkit/attribute_extensions/options.rb +0 -131
- data/lib/castkit/attribute_extensions/serialization.rb +0 -89
- data/lib/castkit/attribute_extensions/validation.rb +0 -72
- data/lib/castkit/data_object_extensions/config.rb +0 -113
- data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
- data/lib/castkit/default_serializer.rb +0 -123
- data/lib/castkit/serializer.rb +0 -92
- data/lib/castkit/validators.rb +0 -4
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
module Ext
|
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
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
module Ext
|
5
|
+
module DataObject
|
6
|
+
# Provides per-class serialization configuration for Castkit::Dataobject, including
|
7
|
+
# root key handling and ignore rules.
|
8
|
+
module Serialization
|
9
|
+
# Automatically extends class-level methods when included.
|
10
|
+
#
|
11
|
+
# @param base [Class]
|
12
|
+
def self.included(base)
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Class-level configuration methods.
|
17
|
+
module ClassMethods
|
18
|
+
# Sets or retrieves the root key to wrap the object under during (de)serialization.
|
19
|
+
#
|
20
|
+
# @param value [String, Symbol, nil] optional root key
|
21
|
+
# @return [Symbol, nil]
|
22
|
+
def root(value = nil)
|
23
|
+
value.nil? ? @root : (@root = value.to_s.strip.to_sym)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sets or retrieves whether to skip `nil` values in output.
|
27
|
+
#
|
28
|
+
# @param value [Boolean, nil]
|
29
|
+
# @return [Boolean, nil]
|
30
|
+
def ignore_nil(value = nil)
|
31
|
+
value.nil? ? @ignore_nil : (@ignore_nil = value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sets or retrieves whether to skip blank values (`[]`, `{}`, `""`, etc.) in output.
|
35
|
+
#
|
36
|
+
# Defaults to true unless explicitly set to false.
|
37
|
+
#
|
38
|
+
# @param value [Boolean, nil]
|
39
|
+
# @return [Boolean]
|
40
|
+
def ignore_blank(value = nil)
|
41
|
+
@ignore_blank = value.nil? || value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the root key for this instance.
|
46
|
+
#
|
47
|
+
# @return [Symbol]
|
48
|
+
def root_key
|
49
|
+
self.class.root.to_s.strip.to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
# Whether a root key is configured for this instance.
|
53
|
+
#
|
54
|
+
# @return [Boolean]
|
55
|
+
def root_key_set?
|
56
|
+
!self.class.root.to_s.strip.empty?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
# Provides string transformation utilities used internally by Castkit
|
5
|
+
module Inflector
|
6
|
+
class << self
|
7
|
+
# Returns the unqualified class name from a namespaced class.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# Castkit::Inflector.class_name(Foo::Bar) # => "Bar"
|
11
|
+
#
|
12
|
+
# @param klass [Class]
|
13
|
+
# @return [String]
|
14
|
+
def unqualified_name(klass)
|
15
|
+
klass.name.to_s.split("::").last
|
16
|
+
end
|
17
|
+
|
18
|
+
# Converts a snake_case or underscored string into PascalCase.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# Castkit::Inflector.pascalize("user_contract") # => "UserContract"
|
22
|
+
# Castkit::Inflector.pascalize(:admin_dto) # => "AdminDto"
|
23
|
+
#
|
24
|
+
# @param string [String, Symbol] the input to convert
|
25
|
+
# @return [String] the PascalCase representation
|
26
|
+
def pascalize(string)
|
27
|
+
underscore(string).to_s.split("_").map(&:capitalize).join
|
28
|
+
end
|
29
|
+
|
30
|
+
# Converts a PascalCase or camelCase string to snake_case.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# Castkit::Inflector.underscore("UserContract") # => "user_contract"
|
34
|
+
# Castkit::Inflector.underscore("XMLParser") # => "xml_parser"
|
35
|
+
#
|
36
|
+
# @param string [String, Symbol]
|
37
|
+
# @return [String]
|
38
|
+
def underscore(string)
|
39
|
+
string
|
40
|
+
.to_s
|
41
|
+
.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
42
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
43
|
+
.downcase
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -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
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
module Types
|
5
|
+
# Abstract base class for type definitions in Castkit.
|
6
|
+
#
|
7
|
+
# Provides default behavior for (de)serialization, validation, and coercion.
|
8
|
+
# All primitive types should subclass this and override methods as needed.
|
9
|
+
#
|
10
|
+
# The `cast!` method is the primary entry point used by attribute processing
|
11
|
+
# to validate and coerce values in a predictable order.
|
12
|
+
class Base
|
13
|
+
class << self
|
14
|
+
# Coerces and validates a value for use in a Castkit DataObject.
|
15
|
+
#
|
16
|
+
# When `force_type` is true, the value is deserialized (coerced) first,
|
17
|
+
# then validated. This is useful when a value may need to be converted
|
18
|
+
# before it can pass validation (e.g. `"123"` → `123`).
|
19
|
+
#
|
20
|
+
# Otherwise, the raw value is validated before coercion.
|
21
|
+
#
|
22
|
+
# @param value [Object] the input value
|
23
|
+
# @param validator [#call, nil] optional custom validator (default uses `validate!`)
|
24
|
+
# @param options [Hash] options passed to `validate!`, e.g., `min`, `max`, `force_type`
|
25
|
+
# @param context [Symbol, String, nil] context label for error messages
|
26
|
+
# @return [Object] the deserialized and validated value
|
27
|
+
def cast!(value, validator: nil, options: {}, context: {})
|
28
|
+
instance = new
|
29
|
+
validator ||= options.delete(:validator)
|
30
|
+
validator ||= default_validator(instance)
|
31
|
+
|
32
|
+
if options[:force_type]
|
33
|
+
deserialized_value = instance.deserialize(value)
|
34
|
+
validator.call(deserialized_value, options: options, context: context)
|
35
|
+
return deserialized_value
|
36
|
+
end
|
37
|
+
|
38
|
+
validator.call(value, options: options, context: context)
|
39
|
+
instance.deserialize(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Deserializes the value using the default type behavior.
|
43
|
+
#
|
44
|
+
# @param value [Object]
|
45
|
+
# @return [Object] the coerced value
|
46
|
+
def deserialize(value)
|
47
|
+
new.deserialize(value)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Serializes the value using the default type behavior.
|
51
|
+
#
|
52
|
+
# @param value [Object]
|
53
|
+
# @return [Object]
|
54
|
+
def serialize(value)
|
55
|
+
new.serialize(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validates the value using the default validator.
|
59
|
+
#
|
60
|
+
# @param value [Object] the value to check
|
61
|
+
# @param options [Hash] validation rules (e.g., min, max, format)
|
62
|
+
# @param context [Symbol, String] label for error reporting
|
63
|
+
# @return [void]
|
64
|
+
def validate!(value, options: {}, context: {})
|
65
|
+
new.validate!(value, options: options, context: context)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Builds a default validator from the instance itself.
|
71
|
+
#
|
72
|
+
# @param instance [Castkit::Types::Base]
|
73
|
+
# @return [Proc] a lambda wrapping `#validate!`
|
74
|
+
def default_validator(instance)
|
75
|
+
lambda do |value, options: {}, context: nil|
|
76
|
+
instance.validate!(value, options: options, context: context)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Deserializes the value. Override in subclasses to coerce input (e.g., `"123"` → `123`).
|
82
|
+
#
|
83
|
+
# @param value [Object]
|
84
|
+
# @return [Object]
|
85
|
+
def deserialize(value)
|
86
|
+
value
|
87
|
+
end
|
88
|
+
|
89
|
+
# Serializes the value. Override in subclasses if the output should be transformed.
|
90
|
+
#
|
91
|
+
# @param value [Object]
|
92
|
+
# @return [Object]
|
93
|
+
def serialize(value)
|
94
|
+
value
|
95
|
+
end
|
96
|
+
|
97
|
+
# Validates the value. No-op by default.
|
98
|
+
#
|
99
|
+
# @param value [Object]
|
100
|
+
# @param options [Hash]
|
101
|
+
# @param context [Symbol, String]
|
102
|
+
# @return [void]
|
103
|
+
def validate!(value, options: {}, context: {})
|
104
|
+
# override in subclasses
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
# Emits or raises a type error depending on configuration.
|
110
|
+
#
|
111
|
+
# @param type [Symbol]
|
112
|
+
# @param value [Object, nil]
|
113
|
+
# @return [void]
|
114
|
+
def type_error!(type, value, context: nil)
|
115
|
+
message = "#{context || "value"} must be a #{type}, got #{value}"
|
116
|
+
raise Castkit::AttributeError, message if Castkit.configuration.raise_type_errors
|
117
|
+
|
118
|
+
Castkit.warning "[Castkit] #{message}" if Castkit.configuration.enable_warnings
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require_relative "../validators/boolean_validator"
|
5
|
+
|
6
|
+
module Castkit
|
7
|
+
module Types
|
8
|
+
# Type definition for `:boolean` attributes.
|
9
|
+
#
|
10
|
+
# Converts strings or numbers into boolean values based on common truthy/falsy indicators.
|
11
|
+
#
|
12
|
+
# This class is used internally by Castkit when an attribute is defined with:
|
13
|
+
# `boolean :is_active`
|
14
|
+
class Boolean < Base
|
15
|
+
# Deserializes the input into a boolean value.
|
16
|
+
#
|
17
|
+
# Accepts:
|
18
|
+
# - `"true"`, `"1"` (case-insensitive) → `true`
|
19
|
+
# - `"false"`, `"0"` (case-insensitive) → `false`
|
20
|
+
#
|
21
|
+
# @param value [Object]
|
22
|
+
# @return [Boolean]
|
23
|
+
# @raise [Castkit::TypeError] if the value cannot be coerced to a boolean
|
24
|
+
def deserialize(value)
|
25
|
+
value
|
26
|
+
end
|
27
|
+
|
28
|
+
# Serializes the boolean value (pass-through).
|
29
|
+
#
|
30
|
+
# @param value [Boolean]
|
31
|
+
# @return [Boolean]
|
32
|
+
def serialize(value)
|
33
|
+
value
|
34
|
+
end
|
35
|
+
|
36
|
+
# Validates the Boolean value.
|
37
|
+
#
|
38
|
+
# @param value [Object]
|
39
|
+
# @param options [Hash] validation options
|
40
|
+
# @param context [Symbol, String] attribute context for error messages
|
41
|
+
# @return [void]
|
42
|
+
def validate!(value, options: {}, context: {})
|
43
|
+
Castkit::Validators::BooleanValidator.call(value, options: options, context: context)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|