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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +196 -210
  3. data/CHANGELOG.md +71 -0
  4. data/README.md +470 -85
  5. data/lib/castkit/attribute.rb +6 -24
  6. data/lib/castkit/castkit.rb +65 -5
  7. data/lib/castkit/configuration.rb +98 -46
  8. data/lib/castkit/contract/data_object.rb +62 -0
  9. data/lib/castkit/contract/generic.rb +168 -0
  10. data/lib/castkit/contract/result.rb +74 -0
  11. data/lib/castkit/contract/validator.rb +248 -0
  12. data/lib/castkit/contract.rb +67 -0
  13. data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
  14. data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
  15. data/lib/castkit/core/config.rb +74 -0
  16. data/lib/castkit/core/registerable.rb +59 -0
  17. data/lib/castkit/data_object.rb +59 -43
  18. data/lib/castkit/default_serializer.rb +87 -32
  19. data/lib/castkit/error.rb +15 -3
  20. data/lib/castkit/ext/attribute/access.rb +67 -0
  21. data/lib/castkit/ext/attribute/error_handling.rb +63 -0
  22. data/lib/castkit/ext/attribute/options.rb +142 -0
  23. data/lib/castkit/ext/attribute/validation.rb +85 -0
  24. data/lib/castkit/ext/data_object/contract.rb +96 -0
  25. data/lib/castkit/ext/data_object/deserialization.rb +167 -0
  26. data/lib/castkit/ext/data_object/serialization.rb +61 -0
  27. data/lib/castkit/inflector.rb +47 -0
  28. data/lib/castkit/types/boolean.rb +43 -0
  29. data/lib/castkit/types/collection.rb +24 -0
  30. data/lib/castkit/types/date.rb +34 -0
  31. data/lib/castkit/types/date_time.rb +34 -0
  32. data/lib/castkit/types/float.rb +46 -0
  33. data/lib/castkit/types/generic.rb +123 -0
  34. data/lib/castkit/types/integer.rb +46 -0
  35. data/lib/castkit/types/string.rb +44 -0
  36. data/lib/castkit/types.rb +15 -0
  37. data/lib/castkit/validators/base_validator.rb +39 -0
  38. data/lib/castkit/validators/numeric_validator.rb +2 -2
  39. data/lib/castkit/validators/string_validator.rb +3 -3
  40. data/lib/castkit/version.rb +1 -1
  41. data/lib/castkit.rb +2 -0
  42. metadata +29 -14
  43. data/castkit-0.1.0.gem +0 -0
  44. data/lib/castkit/attribute_extensions/access.rb +0 -65
  45. data/lib/castkit/attribute_extensions/casting.rb +0 -147
  46. data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
  47. data/lib/castkit/attribute_extensions/options.rb +0 -131
  48. data/lib/castkit/attribute_extensions/serialization.rb +0 -89
  49. data/lib/castkit/attribute_extensions/validation.rb +0 -72
  50. data/lib/castkit/data_object_extensions/config.rb +0 -105
  51. data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
  52. 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
@@ -1,4 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "validators/numeric_validator"
4
- require_relative "validators/string_validator"