castkit 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rspec_status +208 -0
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +195 -0
- data/Rakefile +12 -0
- data/castkit.gemspec +41 -0
- data/lib/castkit/attribute.rb +131 -0
- data/lib/castkit/attribute_extensions/access.rb +65 -0
- data/lib/castkit/attribute_extensions/casting.rb +147 -0
- data/lib/castkit/attribute_extensions/error_handling.rb +83 -0
- data/lib/castkit/attribute_extensions/options.rb +124 -0
- data/lib/castkit/attribute_extensions/serialization.rb +89 -0
- data/lib/castkit/attribute_extensions/validation.rb +72 -0
- data/lib/castkit/castkit.rb +44 -0
- data/lib/castkit/configuration.rb +96 -0
- data/lib/castkit/data_object.rb +153 -0
- data/lib/castkit/data_object_extensions/attribute_types.rb +108 -0
- data/lib/castkit/data_object_extensions/attributes.rb +179 -0
- data/lib/castkit/data_object_extensions/config.rb +105 -0
- data/lib/castkit/data_object_extensions/deserialization.rb +110 -0
- data/lib/castkit/default_serializer.rb +99 -0
- data/lib/castkit/error.rb +42 -0
- data/lib/castkit/serializer.rb +92 -0
- data/lib/castkit/validator.rb +37 -0
- data/lib/castkit/validators/numeric_validator.rb +29 -0
- data/lib/castkit/validators/string_validator.rb +34 -0
- data/lib/castkit/validators.rb +4 -0
- data/lib/castkit/version.rb +5 -0
- data/lib/castkit.rb +19 -0
- data/sig/castkit.rbs +4 -0
- metadata +124 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
module DataObjectExtensions
|
5
|
+
# Provides per-class configuration for a Castkit::DataObject,
|
6
|
+
# including root key handling, strict mode, and unknown key behavior.
|
7
|
+
module Config
|
8
|
+
# Automatically extends class-level methods when included.
|
9
|
+
#
|
10
|
+
# @param base [Class]
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Class-level configuration methods.
|
16
|
+
module ClassMethods
|
17
|
+
# Sets or retrieves the root key to wrap the object under during (de)serialization.
|
18
|
+
#
|
19
|
+
# @param value [String, Symbol, nil] optional root key
|
20
|
+
# @return [Symbol, nil]
|
21
|
+
def root(value = nil)
|
22
|
+
@root = value.to_s.strip.to_sym if value
|
23
|
+
@root
|
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
|
+
|
44
|
+
# Sets or retrieves strict mode behavior.
|
45
|
+
#
|
46
|
+
# In strict mode, unknown keys during deserialization raise errors.
|
47
|
+
#
|
48
|
+
# @param value [Boolean, nil]
|
49
|
+
# @return [Boolean]
|
50
|
+
def strict(value = nil)
|
51
|
+
if value.nil?
|
52
|
+
@strict.nil? || @strict
|
53
|
+
else
|
54
|
+
@strict = value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Enables or disables ignoring unknown keys during deserialization.
|
59
|
+
#
|
60
|
+
# This is the inverse of `strict`.
|
61
|
+
#
|
62
|
+
# @param value [Boolean]
|
63
|
+
# @return [void]
|
64
|
+
def ignore_unknown(value = nil)
|
65
|
+
@strict = !value
|
66
|
+
end
|
67
|
+
|
68
|
+
# Sets or retrieves whether to emit warnings when unknown keys are encountered.
|
69
|
+
#
|
70
|
+
# @param value [Boolean, nil]
|
71
|
+
# @return [Boolean, nil]
|
72
|
+
def warn_on_unknown(value = nil)
|
73
|
+
value.nil? ? @warn_unknown_keys : (@warn_unknown_keys = value)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns a relaxed version of the current class with strict mode off.
|
77
|
+
#
|
78
|
+
# Useful for tolerant parsing scenarios.
|
79
|
+
#
|
80
|
+
# @param warn_on_unknown [Boolean]
|
81
|
+
# @return [Class] a subclass with relaxed rules
|
82
|
+
def relaxed(warn_on_unknown: true)
|
83
|
+
klass = Class.new(self)
|
84
|
+
klass.strict(false)
|
85
|
+
klass.warn_on_unknown(warn_on_unknown)
|
86
|
+
klass
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns the root key for this instance.
|
91
|
+
#
|
92
|
+
# @return [Symbol]
|
93
|
+
def root_key
|
94
|
+
self.class.root.to_s.strip.to_sym
|
95
|
+
end
|
96
|
+
|
97
|
+
# Whether a root key is configured for this instance.
|
98
|
+
#
|
99
|
+
# @return [Boolean]
|
100
|
+
def root_key_set?
|
101
|
+
!self.class.root.to_s.strip.empty?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
module DataObjectExtensions
|
5
|
+
# Adds deserialization support for Castkit::DataObject instances.
|
6
|
+
#
|
7
|
+
# Handles attribute loading, alias resolution, and unwrapped field extraction.
|
8
|
+
module Deserialization
|
9
|
+
# Hooks in class methods like `.from_hash` when included.
|
10
|
+
#
|
11
|
+
# @param base [Class]
|
12
|
+
def self.included(base)
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Class-level deserialization helpers.
|
17
|
+
module ClassMethods
|
18
|
+
# Builds a new instance from a Hash, symbolizing keys as needed.
|
19
|
+
#
|
20
|
+
# @param hash [Hash]
|
21
|
+
# @return [Castkit::DataObject]
|
22
|
+
def from_hash(hash)
|
23
|
+
hash = hash.transform_keys { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
|
24
|
+
new(hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!method from_h(hash)
|
28
|
+
# Alias for {.from_hash}
|
29
|
+
#
|
30
|
+
# @!method creator(hash)
|
31
|
+
# Alias for {.from_hash}
|
32
|
+
alias from_h from_hash
|
33
|
+
alias creator from_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Loads attribute values from the given hash.
|
39
|
+
#
|
40
|
+
# Respects access control (e.g., `writeable?`) and uses `.load` for casting/validation.
|
41
|
+
#
|
42
|
+
# @param data [Hash]
|
43
|
+
# @return [void]
|
44
|
+
def deserialize_attributes!(data)
|
45
|
+
self.class.attributes.each do |field, attribute|
|
46
|
+
next if attribute.skip_deserialization?
|
47
|
+
|
48
|
+
value = fetch_attribute_key(data, attribute)
|
49
|
+
value = attribute.load(value, context: field)
|
50
|
+
|
51
|
+
instance_variable_set("@#{field}", value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Fetches the best matching value from the hash using attribute key and aliases.
|
56
|
+
#
|
57
|
+
# @param data [Hash]
|
58
|
+
# @param attribute [Castkit::Attribute]
|
59
|
+
# @return [Object]
|
60
|
+
def fetch_attribute_key(data, attribute)
|
61
|
+
attribute.key_path(with_aliases: true).each do |path|
|
62
|
+
value = path.reduce(data) { |memo, key| memo.is_a?(Hash) ? memo[key] : nil }
|
63
|
+
return value unless value.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# Extracts prefixed fields for unwrapped attributes and groups them under the original field key.
|
70
|
+
#
|
71
|
+
# @param data [Hash]
|
72
|
+
# @return [Hash] modified input hash with unwrapped values nested under their base field
|
73
|
+
def unwrap_prefixed_fields!(data)
|
74
|
+
self.class.attributes.each_value do |attribute|
|
75
|
+
next unless attribute.unwrapped?
|
76
|
+
|
77
|
+
unwrapped, keys_to_remove = unwrap_prefixed_values(data, attribute)
|
78
|
+
next if unwrapped.empty?
|
79
|
+
|
80
|
+
data[attribute.field] = unwrapped
|
81
|
+
keys_to_remove.each { |k| data.delete(k) }
|
82
|
+
end
|
83
|
+
|
84
|
+
data
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the prefixed key-value pairs for a given unwrapped attribute.
|
88
|
+
#
|
89
|
+
# @param data [Hash]
|
90
|
+
# @param attribute [Castkit::Attribute]
|
91
|
+
# @return [Array<Hash, Array<Symbol>>] extracted key-value pairs and keys to delete
|
92
|
+
def unwrap_prefixed_values(data, attribute)
|
93
|
+
prefix = attribute.prefix.to_s
|
94
|
+
unwrapped_data = {}
|
95
|
+
keys_to_remove = []
|
96
|
+
|
97
|
+
data.each do |k, v|
|
98
|
+
k_str = k.to_s
|
99
|
+
next unless k_str.start_with?(prefix)
|
100
|
+
|
101
|
+
stripped = k_str.sub(prefix, "").to_sym
|
102
|
+
unwrapped_data[stripped] = v
|
103
|
+
keys_to_remove << k
|
104
|
+
end
|
105
|
+
|
106
|
+
[unwrapped_data, keys_to_remove]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "serializer"
|
4
|
+
|
5
|
+
module Castkit
|
6
|
+
# Default serializer for Castkit::DataObject instances.
|
7
|
+
#
|
8
|
+
# Serializes attributes based on access rules, nil/blank filtering, and nested structure.
|
9
|
+
class DefaultSerializer < Castkit::Serializer
|
10
|
+
SKIP_ATTRIBUTE = :__castkit_skip_attribute
|
11
|
+
|
12
|
+
# @return [Hash{Symbol => Castkit::Attribute}] attributes to serialize
|
13
|
+
attr_reader :attributes
|
14
|
+
|
15
|
+
# @return [Hash] serialization options (root key, ignore_nil, etc.)
|
16
|
+
attr_reader :options
|
17
|
+
|
18
|
+
# Returns the serialized object as a Hash.
|
19
|
+
#
|
20
|
+
# Includes root wrapping if configured.
|
21
|
+
#
|
22
|
+
# @return [Hash]
|
23
|
+
def call
|
24
|
+
result = serialize_attributes
|
25
|
+
options[:root] ? { options[:root].to_sym => result } : result
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Initializes the serializer with the target object and context.
|
31
|
+
#
|
32
|
+
# @param raw [Castkit::DataObject] the object to serialize
|
33
|
+
# @param visited [Set, nil] used to detect circular references
|
34
|
+
def initialize(raw, visited: nil)
|
35
|
+
super
|
36
|
+
|
37
|
+
@attributes = raw.class.attributes
|
38
|
+
@options = {
|
39
|
+
root: raw.class.root,
|
40
|
+
ignore_nil: raw.class.ignore_nil
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Iterates over attributes and serializes each into a result hash.
|
45
|
+
#
|
46
|
+
# @return [Hash]
|
47
|
+
def serialize_attributes
|
48
|
+
attributes.each_with_object({}) do |(_, attribute), hash|
|
49
|
+
next if attribute.skip_serialization?
|
50
|
+
|
51
|
+
serialized_value = serialize_attribute(attribute)
|
52
|
+
next if serialized_value == SKIP_ATTRIBUTE
|
53
|
+
|
54
|
+
assign_attribute_key!(hash, attribute, serialized_value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Process and serialize a given attribute.
|
59
|
+
#
|
60
|
+
# @param attribute [Castkit::Attribute] The attribute instance.
|
61
|
+
# @param [Object]
|
62
|
+
def serialize_attribute(attribute)
|
63
|
+
value = obj.public_send(attribute.field)
|
64
|
+
return SKIP_ATTRIBUTE if value.nil? && (attribute.ignore_nil? || options[:ignore_nil])
|
65
|
+
|
66
|
+
serialized_value = attribute.dump(value, visited: visited)
|
67
|
+
return SKIP_ATTRIBUTE if blank?(serialized_value) && (attribute.ignore_blank? || options[:ignore_blank])
|
68
|
+
|
69
|
+
serialized_value
|
70
|
+
end
|
71
|
+
|
72
|
+
# Assigns a serialized value into the hash using nested key paths.
|
73
|
+
#
|
74
|
+
# @param hash [Hash]
|
75
|
+
# @param attribute [Castkit::Attribute]
|
76
|
+
# @param value [Object]
|
77
|
+
# @return [void]
|
78
|
+
def assign_attribute_key!(hash, attribute, value)
|
79
|
+
key_path = attribute.key_path
|
80
|
+
last = key_path.pop
|
81
|
+
current = hash
|
82
|
+
|
83
|
+
key_path.each do |key|
|
84
|
+
current[key] ||= {}
|
85
|
+
current = current[key]
|
86
|
+
end
|
87
|
+
|
88
|
+
current[last] = value
|
89
|
+
end
|
90
|
+
|
91
|
+
# Determines if a value is blank (nil or empty).
|
92
|
+
#
|
93
|
+
# @param value [Object, nil]
|
94
|
+
# @return [Boolean]
|
95
|
+
def blank?(value)
|
96
|
+
value.nil? || (value.respond_to?(:empty?) && value&.empty?)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
# Base error class for all Castkit-related exceptions.
|
5
|
+
class Error < StandardError
|
6
|
+
# @return [Hash, Object, nil] contextual data to aid in debugging
|
7
|
+
attr_reader :context
|
8
|
+
|
9
|
+
# Initializes a Castkit error.
|
10
|
+
#
|
11
|
+
# @param msg [String] the error message
|
12
|
+
# @param context [Object, nil] optional data object or hash for context
|
13
|
+
def initialize(msg, context: nil)
|
14
|
+
super(msg)
|
15
|
+
@context = context
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Raised for issues related to Castkit::DataObject initialization or usage.
|
20
|
+
class DataObjectError < Error; end
|
21
|
+
|
22
|
+
# Raised for attribute validation, access, or casting failures.
|
23
|
+
class AttributeError < Error
|
24
|
+
# Returns the field name related to the error, if available.
|
25
|
+
#
|
26
|
+
# @return [Symbol]
|
27
|
+
def field
|
28
|
+
context.is_a?(Hash) ? context[:field] : context || :unknown
|
29
|
+
end
|
30
|
+
|
31
|
+
# Formats the error message with field info if available.
|
32
|
+
#
|
33
|
+
# @return [String]
|
34
|
+
def to_s
|
35
|
+
field_info = field ? " (on #{field})" : ""
|
36
|
+
"#{super}#{field_info}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Raised during serialization if an object fails to serialize properly.
|
41
|
+
class SerializationError < Error; end
|
42
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Castkit
|
6
|
+
# Abstract base class for defining custom serializers for Castkit::DataObject instances.
|
7
|
+
#
|
8
|
+
# Handles circular reference detection and provides a consistent `call` API.
|
9
|
+
#
|
10
|
+
# Subclasses must implement an instance method `#call` that returns a hash-like representation.
|
11
|
+
#
|
12
|
+
# @example Usage
|
13
|
+
# class CustomSerializer < Castkit::Serializer
|
14
|
+
# private
|
15
|
+
#
|
16
|
+
# def call
|
17
|
+
# { type: obj.class.name, data: obj.to_h }
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# CustomSerializer.call(user_dto)
|
22
|
+
class Serializer
|
23
|
+
class << self
|
24
|
+
# Entrypoint for serializing an object.
|
25
|
+
#
|
26
|
+
# @param obj [Castkit::DataObject] the object to serialize
|
27
|
+
# @param visited [Set, nil] used to track visited object IDs
|
28
|
+
# @return [Object] result of custom serialization
|
29
|
+
def call(obj, visited: nil)
|
30
|
+
new(obj, visited: visited).send(:serialize)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Castkit::DataObject] the object being serialized
|
35
|
+
attr_reader :obj
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
# Fallback to the default serializer.
|
40
|
+
#
|
41
|
+
# @return [Hash]
|
42
|
+
def serialize_with_default
|
43
|
+
Castkit::DefaultSerializer.call(obj, visited: visited)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# @return [Set<Integer>] a set of visited object IDs to detect circular references
|
49
|
+
attr_reader :visited
|
50
|
+
|
51
|
+
# Initializes the serializer instance.
|
52
|
+
#
|
53
|
+
# @param obj [Castkit::DataObject]
|
54
|
+
# @param visited [Set, nil]
|
55
|
+
def initialize(obj, visited: nil)
|
56
|
+
@obj = obj
|
57
|
+
@visited = visited || Set.new
|
58
|
+
end
|
59
|
+
|
60
|
+
# Subclasses must override this method to implement serialization logic.
|
61
|
+
#
|
62
|
+
# @raise [NotImplementedError]
|
63
|
+
# @return [Object]
|
64
|
+
def call
|
65
|
+
raise NotImplementedError, "#{self.class.name} must implement `#call`"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Wraps the actual serialization logic with circular reference detection.
|
69
|
+
#
|
70
|
+
# @return [Object]
|
71
|
+
# @raise [Castkit::SerializationError] if a circular reference is detected
|
72
|
+
def serialize
|
73
|
+
check_circular_reference!
|
74
|
+
visited << obj.object_id
|
75
|
+
|
76
|
+
result = call
|
77
|
+
visited.delete(obj.object_id)
|
78
|
+
|
79
|
+
result
|
80
|
+
end
|
81
|
+
|
82
|
+
# Raises if the object has already been visited (circular reference).
|
83
|
+
#
|
84
|
+
# @raise [Castkit::SerializationError]
|
85
|
+
# @return [void]
|
86
|
+
def check_circular_reference!
|
87
|
+
return unless visited.include?(obj.object_id)
|
88
|
+
|
89
|
+
raise Castkit::SerializationError, "Circular reference detected for #{obj.class}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castkit
|
4
|
+
# Abstract base class for all attribute validators.
|
5
|
+
#
|
6
|
+
# Validators ensure that a value conforms to specific rules (e.g., type, format, range).
|
7
|
+
# Subclasses must implement the instance method `#call`.
|
8
|
+
#
|
9
|
+
# @abstract
|
10
|
+
class Validator
|
11
|
+
class << self
|
12
|
+
# Invokes the validator with the given arguments.
|
13
|
+
#
|
14
|
+
# @param value [Object] the attribute value to validate
|
15
|
+
# @param options [Hash] the attribute options (e.g., `min`, `max`, `format`)
|
16
|
+
# @param context [Symbol, String, Hash] the attribute name or context for error reporting
|
17
|
+
# @return [void]
|
18
|
+
# @raise [Castkit::AttributeError] if validation fails
|
19
|
+
def call(value, options:, context:)
|
20
|
+
new.call(value, options: options, context: context)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Validates the attribute value.
|
25
|
+
#
|
26
|
+
# @abstract Override in subclasses.
|
27
|
+
#
|
28
|
+
# @param value [Object] the attribute value to validate
|
29
|
+
# @param options [Hash] the attribute options
|
30
|
+
# @param context [Symbol, String, Hash] the attribute name or context
|
31
|
+
# @return [void]
|
32
|
+
# @raise [NotImplementedError] unless implemented in a subclass
|
33
|
+
def call(value, options:, context:)
|
34
|
+
raise NotImplementedError, "#{self.class.name} must implement `#call`"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../validator"
|
4
|
+
|
5
|
+
module Castkit
|
6
|
+
module Validators
|
7
|
+
# Validates that a numeric value falls within the allowed range.
|
8
|
+
#
|
9
|
+
# Supports `:min` and `:max` options to enforce bounds.
|
10
|
+
class NumericValidator < Castkit::Validator
|
11
|
+
# Validates the numeric value.
|
12
|
+
#
|
13
|
+
# @param value [Numeric] the value to validate
|
14
|
+
# @param options [Hash] validation options (e.g., `min`, `max`)
|
15
|
+
# @param context [Symbol, String] the attribute name or key for error messages
|
16
|
+
# @raise [Castkit::AttributeError] if the value violates min/max bounds
|
17
|
+
# @return [void]
|
18
|
+
def call(value, options:, context:)
|
19
|
+
if options[:min] && value < options[:min]
|
20
|
+
raise Castkit::AttributeError, "#{context} must be >= #{options[:min]}"
|
21
|
+
end
|
22
|
+
|
23
|
+
return unless options[:max] && value > options[:max]
|
24
|
+
|
25
|
+
raise Castkit::AttributeError, "#{context} must be <= #{options[:max]}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../validator"
|
4
|
+
|
5
|
+
module Castkit
|
6
|
+
module Validators
|
7
|
+
# Validates that a value is a String and optionally conforms to a format.
|
8
|
+
#
|
9
|
+
# Supports format validation using a Regexp or a custom Proc.
|
10
|
+
class StringValidator < Castkit::Validator
|
11
|
+
# Validates the string value.
|
12
|
+
#
|
13
|
+
# @param value [Object] the value to validate
|
14
|
+
# @param options [Hash] validation options (e.g., `format: /regex/` or `format: ->(v) { ... }`)
|
15
|
+
# @param context [Symbol, String] the attribute name or key for error messages
|
16
|
+
# @raise [Castkit::AttributeError] if value is not a string or fails format validation
|
17
|
+
# @return [void]
|
18
|
+
def call(value, options:, context:)
|
19
|
+
raise Castkit::AttributeError, "#{context} must be a String" unless value.is_a?(String)
|
20
|
+
|
21
|
+
return unless options[:format]
|
22
|
+
|
23
|
+
case options[:format]
|
24
|
+
when Regexp
|
25
|
+
raise Castkit::AttributeError, "#{context} must match format" unless value =~ options[:format]
|
26
|
+
when Proc
|
27
|
+
raise Castkit::AttributeError, "#{context} failed format validation" unless options[:format].call(value)
|
28
|
+
else
|
29
|
+
raise Castkit::AttributeError, "#{context} has unsupported format validator: #{options[:format].class}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/castkit.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "castkit/version"
|
4
|
+
require_relative "castkit/data_object"
|
5
|
+
|
6
|
+
# Castkit is a lightweight, type-safe data object system for Ruby.
|
7
|
+
#
|
8
|
+
# It provides a declarative DSL for defining DTOs with typecasting, validation,
|
9
|
+
# access control, serialization, deserialization, and OpenAPI-friendly schema generation.
|
10
|
+
#
|
11
|
+
# @example Defining a simple data object
|
12
|
+
# class UserDto < Castkit::DataObject
|
13
|
+
# string :name
|
14
|
+
# integer :age, required: false
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# user = UserDto.new(name: "Alice", age: 30)
|
18
|
+
# user.to_h #=> { name: "Alice", age: 30 }
|
19
|
+
module Castkit; end
|
data/sig/castkit.rbs
ADDED