castkit 0.1.1 → 0.2.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 +196 -210
- data/CHANGELOG.md +71 -0
- data/README.md +470 -85
- data/lib/castkit/attribute.rb +6 -24
- data/lib/castkit/castkit.rb +65 -5
- data/lib/castkit/configuration.rb +98 -46
- data/lib/castkit/contract/data_object.rb +62 -0
- data/lib/castkit/contract/generic.rb +168 -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 +59 -43
- data/lib/castkit/default_serializer.rb +87 -32
- 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/serialization.rb +61 -0
- data/lib/castkit/inflector.rb +47 -0
- data/lib/castkit/types/boolean.rb +43 -0
- data/lib/castkit/types/collection.rb +24 -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/generic.rb +123 -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_validator.rb +39 -0
- data/lib/castkit/validators/numeric_validator.rb +2 -2
- data/lib/castkit/validators/string_validator.rb +3 -3
- data/lib/castkit/version.rb +1 -1
- data/lib/castkit.rb +2 -0
- metadata +29 -14
- data/castkit-0.1.0.gem +0 -0
- 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 -105
- data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
- data/lib/castkit/validators.rb +0 -4
@@ -1,72 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "error_handling"
|
4
|
-
require_relative "options"
|
5
|
-
|
6
|
-
module Castkit
|
7
|
-
module AttributeExtensions
|
8
|
-
# Provides validation logic for attribute configuration.
|
9
|
-
#
|
10
|
-
# These checks are typically performed at attribute initialization to catch misconfigurations early.
|
11
|
-
module Validation
|
12
|
-
include Castkit::AttributeExtensions::ErrorHandling
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
# Runs all validation checks on the attribute definition.
|
17
|
-
#
|
18
|
-
# This includes:
|
19
|
-
# - Custom validator integrity
|
20
|
-
# - Access mode validity
|
21
|
-
# - Unwrapped prefix usage
|
22
|
-
# - Array `of:` type presence
|
23
|
-
#
|
24
|
-
# @return [void]
|
25
|
-
def validate!
|
26
|
-
validate_custom_validator!
|
27
|
-
validate_access!
|
28
|
-
validate_unwrapped_options!
|
29
|
-
validate_array_options!
|
30
|
-
end
|
31
|
-
|
32
|
-
# Validates the presence and interface of a custom validator.
|
33
|
-
#
|
34
|
-
# @return [void]
|
35
|
-
# @raise [Castkit::AttributeError] if the validator is not callable
|
36
|
-
def validate_custom_validator!
|
37
|
-
return unless options[:validator]
|
38
|
-
return if options[:validator].respond_to?(:call)
|
39
|
-
|
40
|
-
raise_error!("Custom validator for `#{field}` must respond to `.call`")
|
41
|
-
end
|
42
|
-
|
43
|
-
# Validates that each declared access mode is valid.
|
44
|
-
#
|
45
|
-
# @return [void]
|
46
|
-
# @raise [Castkit::AttributeError] if any access mode is invalid and enforcement is enabled
|
47
|
-
def validate_access!
|
48
|
-
access.each do |mode|
|
49
|
-
next if Castkit::AttributeExtensions::Options::DEFAULT_OPTIONS[:access].include?(mode)
|
50
|
-
|
51
|
-
handle_error(:access, mode: mode, context: to_h)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# Ensures prefix is only used with unwrapped attributes.
|
56
|
-
#
|
57
|
-
# @return [void]
|
58
|
-
# @raise [Castkit::AttributeError] if prefix is used without `unwrapped: true`
|
59
|
-
def validate_unwrapped_options!
|
60
|
-
handle_error(:unwrapped, context: to_h) if prefix && !unwrapped?
|
61
|
-
end
|
62
|
-
|
63
|
-
# Ensures `of:` is provided for array-typed attributes.
|
64
|
-
#
|
65
|
-
# @return [void]
|
66
|
-
# @raise [Castkit::AttributeError] if `of:` is missing for `type: :array`
|
67
|
-
def validate_array_options!
|
68
|
-
handle_error(:array_options, context: to_h) if type == :array && options[:of].nil?
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
@@ -1,105 +0,0 @@
|
|
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
|
@@ -1,110 +0,0 @@
|
|
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
|
data/lib/castkit/validators.rb
DELETED