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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +195 -219
  3. data/CHANGELOG.md +42 -0
  4. data/README.md +744 -83
  5. data/castkit.gemspec +1 -0
  6. data/lib/castkit/attribute.rb +6 -24
  7. data/lib/castkit/castkit.rb +61 -10
  8. data/lib/castkit/cli/generate.rb +98 -0
  9. data/lib/castkit/cli/list.rb +200 -0
  10. data/lib/castkit/cli/main.rb +43 -0
  11. data/lib/castkit/cli.rb +24 -0
  12. data/lib/castkit/configuration.rb +116 -46
  13. data/lib/castkit/contract/base.rb +168 -0
  14. data/lib/castkit/contract/data_object.rb +62 -0
  15. data/lib/castkit/contract/result.rb +74 -0
  16. data/lib/castkit/contract/validator.rb +248 -0
  17. data/lib/castkit/contract.rb +67 -0
  18. data/lib/castkit/{data_object_extensions → core}/attribute_types.rb +21 -7
  19. data/lib/castkit/{data_object_extensions → core}/attributes.rb +8 -3
  20. data/lib/castkit/core/config.rb +74 -0
  21. data/lib/castkit/core/registerable.rb +59 -0
  22. data/lib/castkit/data_object.rb +56 -67
  23. data/lib/castkit/error.rb +15 -3
  24. data/lib/castkit/ext/attribute/access.rb +67 -0
  25. data/lib/castkit/ext/attribute/error_handling.rb +63 -0
  26. data/lib/castkit/ext/attribute/options.rb +142 -0
  27. data/lib/castkit/ext/attribute/validation.rb +85 -0
  28. data/lib/castkit/ext/data_object/contract.rb +96 -0
  29. data/lib/castkit/ext/data_object/deserialization.rb +167 -0
  30. data/lib/castkit/ext/data_object/plugins.rb +86 -0
  31. data/lib/castkit/ext/data_object/serialization.rb +61 -0
  32. data/lib/castkit/inflector.rb +47 -0
  33. data/lib/castkit/plugins.rb +82 -0
  34. data/lib/castkit/serializers/base.rb +94 -0
  35. data/lib/castkit/serializers/default_serializer.rb +156 -0
  36. data/lib/castkit/types/base.rb +122 -0
  37. data/lib/castkit/types/boolean.rb +47 -0
  38. data/lib/castkit/types/collection.rb +35 -0
  39. data/lib/castkit/types/date.rb +34 -0
  40. data/lib/castkit/types/date_time.rb +34 -0
  41. data/lib/castkit/types/float.rb +46 -0
  42. data/lib/castkit/types/integer.rb +46 -0
  43. data/lib/castkit/types/string.rb +44 -0
  44. data/lib/castkit/types.rb +15 -0
  45. data/lib/castkit/validators/base.rb +59 -0
  46. data/lib/castkit/validators/boolean_validator.rb +39 -0
  47. data/lib/castkit/validators/collection_validator.rb +29 -0
  48. data/lib/castkit/validators/float_validator.rb +31 -0
  49. data/lib/castkit/validators/integer_validator.rb +31 -0
  50. data/lib/castkit/validators/numeric_validator.rb +2 -2
  51. data/lib/castkit/validators/string_validator.rb +3 -4
  52. data/lib/castkit/version.rb +1 -1
  53. data/lib/castkit.rb +2 -0
  54. data/lib/generators/base.rb +97 -0
  55. data/lib/generators/contract.rb +68 -0
  56. data/lib/generators/data_object.rb +48 -0
  57. data/lib/generators/plugin.rb +25 -0
  58. data/lib/generators/serializer.rb +28 -0
  59. data/lib/generators/templates/contract.rb.tt +24 -0
  60. data/lib/generators/templates/contract_spec.rb.tt +76 -0
  61. data/lib/generators/templates/data_object.rb.tt +15 -0
  62. data/lib/generators/templates/data_object_spec.rb.tt +36 -0
  63. data/lib/generators/templates/plugin.rb.tt +37 -0
  64. data/lib/generators/templates/plugin_spec.rb.tt +18 -0
  65. data/lib/generators/templates/serializer.rb.tt +24 -0
  66. data/lib/generators/templates/serializer_spec.rb.tt +14 -0
  67. data/lib/generators/templates/type.rb.tt +55 -0
  68. data/lib/generators/templates/type_spec.rb.tt +42 -0
  69. data/lib/generators/templates/validator.rb.tt +26 -0
  70. data/lib/generators/templates/validator_spec.rb.tt +23 -0
  71. data/lib/generators/type.rb +29 -0
  72. data/lib/generators/validator.rb +41 -0
  73. metadata +74 -15
  74. data/lib/castkit/attribute_extensions/access.rb +0 -65
  75. data/lib/castkit/attribute_extensions/casting.rb +0 -147
  76. data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
  77. data/lib/castkit/attribute_extensions/options.rb +0 -131
  78. data/lib/castkit/attribute_extensions/serialization.rb +0 -89
  79. data/lib/castkit/attribute_extensions/validation.rb +0 -72
  80. data/lib/castkit/data_object_extensions/config.rb +0 -113
  81. data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
  82. data/lib/castkit/default_serializer.rb +0 -123
  83. data/lib/castkit/serializer.rb +0 -92
  84. data/lib/castkit/validators.rb +0 -4
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: castkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Lucas
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-14 00:00:00.000000000 Z
11
+ date: 2025-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rspec
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -72,27 +86,72 @@ files:
72
86
  - castkit.gemspec
73
87
  - lib/castkit.rb
74
88
  - lib/castkit/attribute.rb
75
- - lib/castkit/attribute_extensions/access.rb
76
- - lib/castkit/attribute_extensions/casting.rb
77
- - lib/castkit/attribute_extensions/error_handling.rb
78
- - lib/castkit/attribute_extensions/options.rb
79
- - lib/castkit/attribute_extensions/serialization.rb
80
- - lib/castkit/attribute_extensions/validation.rb
81
89
  - lib/castkit/castkit.rb
90
+ - lib/castkit/cli.rb
91
+ - lib/castkit/cli/generate.rb
92
+ - lib/castkit/cli/list.rb
93
+ - lib/castkit/cli/main.rb
82
94
  - lib/castkit/configuration.rb
95
+ - lib/castkit/contract.rb
96
+ - lib/castkit/contract/base.rb
97
+ - lib/castkit/contract/data_object.rb
98
+ - lib/castkit/contract/result.rb
99
+ - lib/castkit/contract/validator.rb
100
+ - lib/castkit/core/attribute_types.rb
101
+ - lib/castkit/core/attributes.rb
102
+ - lib/castkit/core/config.rb
103
+ - lib/castkit/core/registerable.rb
83
104
  - lib/castkit/data_object.rb
84
- - lib/castkit/data_object_extensions/attribute_types.rb
85
- - lib/castkit/data_object_extensions/attributes.rb
86
- - lib/castkit/data_object_extensions/config.rb
87
- - lib/castkit/data_object_extensions/deserialization.rb
88
- - lib/castkit/default_serializer.rb
89
105
  - lib/castkit/error.rb
90
- - lib/castkit/serializer.rb
106
+ - lib/castkit/ext/attribute/access.rb
107
+ - lib/castkit/ext/attribute/error_handling.rb
108
+ - lib/castkit/ext/attribute/options.rb
109
+ - lib/castkit/ext/attribute/validation.rb
110
+ - lib/castkit/ext/data_object/contract.rb
111
+ - lib/castkit/ext/data_object/deserialization.rb
112
+ - lib/castkit/ext/data_object/plugins.rb
113
+ - lib/castkit/ext/data_object/serialization.rb
114
+ - lib/castkit/inflector.rb
115
+ - lib/castkit/plugins.rb
116
+ - lib/castkit/serializers/base.rb
117
+ - lib/castkit/serializers/default_serializer.rb
118
+ - lib/castkit/types.rb
119
+ - lib/castkit/types/base.rb
120
+ - lib/castkit/types/boolean.rb
121
+ - lib/castkit/types/collection.rb
122
+ - lib/castkit/types/date.rb
123
+ - lib/castkit/types/date_time.rb
124
+ - lib/castkit/types/float.rb
125
+ - lib/castkit/types/integer.rb
126
+ - lib/castkit/types/string.rb
91
127
  - lib/castkit/validator.rb
92
- - lib/castkit/validators.rb
128
+ - lib/castkit/validators/base.rb
129
+ - lib/castkit/validators/boolean_validator.rb
130
+ - lib/castkit/validators/collection_validator.rb
131
+ - lib/castkit/validators/float_validator.rb
132
+ - lib/castkit/validators/integer_validator.rb
93
133
  - lib/castkit/validators/numeric_validator.rb
94
134
  - lib/castkit/validators/string_validator.rb
95
135
  - lib/castkit/version.rb
136
+ - lib/generators/base.rb
137
+ - lib/generators/contract.rb
138
+ - lib/generators/data_object.rb
139
+ - lib/generators/plugin.rb
140
+ - lib/generators/serializer.rb
141
+ - lib/generators/templates/contract.rb.tt
142
+ - lib/generators/templates/contract_spec.rb.tt
143
+ - lib/generators/templates/data_object.rb.tt
144
+ - lib/generators/templates/data_object_spec.rb.tt
145
+ - lib/generators/templates/plugin.rb.tt
146
+ - lib/generators/templates/plugin_spec.rb.tt
147
+ - lib/generators/templates/serializer.rb.tt
148
+ - lib/generators/templates/serializer_spec.rb.tt
149
+ - lib/generators/templates/type.rb.tt
150
+ - lib/generators/templates/type_spec.rb.tt
151
+ - lib/generators/templates/validator.rb.tt
152
+ - lib/generators/templates/validator_spec.rb.tt
153
+ - lib/generators/type.rb
154
+ - lib/generators/validator.rb
96
155
  - sig/castkit.rbs
97
156
  homepage: https://github.com/bnlucas/castkit
98
157
  licenses:
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castkit
4
- module AttributeExtensions
5
- # Provides access control helpers for attributes.
6
- #
7
- # These helpers determine whether an attribute is readable, writeable,
8
- # or should be included during serialization/deserialization based on the
9
- # configured `:access` and `:ignore` options.
10
- module Access
11
- # Returns the normalized access modes for the attribute (e.g., [:read, :write]).
12
- #
13
- # @return [Array<Symbol>] list of access symbols
14
- def access
15
- Array(options[:access]).map(&:to_sym)
16
- end
17
-
18
- # Whether the attribute should be included during serialization.
19
- #
20
- # @return [Boolean]
21
- def readable?
22
- access.include?(:read)
23
- end
24
-
25
- # Whether the attribute should be accepted during deserialization.
26
- #
27
- # Composite attributes are excluded from writeability.
28
- #
29
- # @return [Boolean]
30
- def writeable?
31
- access.include?(:write) && !composite?
32
- end
33
-
34
- # Whether the attribute is both readable and writeable.
35
- #
36
- # @return [Boolean]
37
- def full_access?
38
- readable? && writeable?
39
- end
40
-
41
- # Whether the attribute should be skipped during serialization.
42
- #
43
- # This is true if it's not readable or is marked as ignored.
44
- #
45
- # @return [Boolean]
46
- def skip_serialization?
47
- !readable? || ignore?
48
- end
49
-
50
- # Whether the attribute should be skipped during deserialization.
51
- #
52
- # @return [Boolean]
53
- def skip_deserialization?
54
- !writeable?
55
- end
56
-
57
- # Whether the attribute is ignored completely.
58
- #
59
- # @return [Boolean]
60
- def ignore?
61
- options[:ignore]
62
- end
63
- end
64
- end
65
- end
@@ -1,147 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "date"
4
- require_relative "../data_object"
5
- require_relative "error_handling"
6
-
7
- module Castkit
8
- module AttributeExtensions
9
- # Provides typecasting logic for attributes based on their declared type.
10
- #
11
- # Supports primitive types, arrays, nested Castkit::DataObject types, and union types.
12
- module Casting
13
- include Castkit::AttributeExtensions::ErrorHandling
14
-
15
- PRIMITIVE_CASTERS = {
16
- integer: lambda(&:to_i),
17
- float: lambda(&:to_f),
18
- string: lambda(&:to_s),
19
- hash: ->(v) { v },
20
- array: ->(v) { Array(v) }
21
- }.freeze
22
-
23
- private
24
-
25
- # Casts a value based on the attribute's declared type.
26
- #
27
- # @param value [Object] the input value to cast
28
- # @return [Object, nil] the cast value
29
- # @raise [Castkit::AttributeError] if the value cannot be cast
30
- def cast(value)
31
- handle_error(:array_of_type) if type == :array && options[:of].nil?
32
- value = default if value.nil?
33
- return if value.nil? && optional?
34
-
35
- cast_value(value)
36
- end
37
-
38
- # Delegates casting logic based on the attribute's type.
39
- #
40
- # This method is invoked internally by `#cast` after nil handling and default fallback.
41
- #
42
- # - For union types (`Array` of types), attempts each in order.
43
- # - For array types, maps over elements with `#cast_element`.
44
- # - For nested data objects, delegates to `.cast`.
45
- # - For primitive types, uses `#cast_primitive`.
46
- #
47
- # @param value [Object] the raw input value to cast
48
- # @return [Object, nil] the cast value
49
- # @raise [Castkit::AttributeError] if casting fails for all union types or invalid primitives
50
- def cast_value(value)
51
- if type.is_a?(Array)
52
- try_union_cast(value)
53
- elsif type == :array
54
- Array(value).map { |v| cast_element(v) }
55
- elsif dataobject?
56
- type.cast(value)
57
- else
58
- cast_primitive(value)
59
- end
60
- end
61
-
62
- # Attempts to cast the value against a union of possible types.
63
- #
64
- # @param value [Object]
65
- # @return [Object] the first successful cast result
66
- # @raise [Castkit::AttributeError] if no types match
67
- def try_union_cast(value)
68
- last_error = nil
69
-
70
- type.each do |t|
71
- return try_cast_type(value, t)
72
- rescue Castkit::AttributeError => e
73
- last_error = e
74
- end
75
-
76
- raise last_error || handle_error(:union, types: type)
77
- end
78
-
79
- # Tries to cast a value to a specific type.
80
- #
81
- # @param value [Object]
82
- # @param type [Symbol, Class] the type to try
83
- # @return [Object, nil]
84
- def try_cast_type(value, type)
85
- return type.cast(value) if Castkit.dataobject?(type)
86
-
87
- cast_primitive(value, type: type)
88
- end
89
-
90
- # Casts an element of an array attribute.
91
- #
92
- # @param value [Object]
93
- # @return [Object, nil]
94
- def cast_element(value)
95
- if Castkit.dataobject?(options[:of])
96
- options[:of].cast(value)
97
- else
98
- validate_element_type!(value)
99
- cast_primitive(value, type: options[:of])
100
- end
101
- end
102
-
103
- # Casts a primitive value based on its type.
104
- #
105
- # @param value [Object]
106
- # @param type [Symbol]
107
- # @return [Object, nil]
108
- # @raise [Castkit::AttributeError]
109
- def cast_primitive(value, type: self.type)
110
- return cast_boolean(value) if type == :boolean
111
- return Date.parse(value.to_s) if type == :date
112
- return DateTime.parse(value.to_s) if type == :datetime
113
-
114
- PRIMITIVE_CASTERS.fetch(type) do
115
- handle_error(:primitive, type: type)
116
- end.call(value)
117
- end
118
-
119
- # Casts a value to boolean.
120
- #
121
- # @param value [Object]
122
- # @return [Boolean, nil]
123
- # @raise [Castkit::AttributeError]
124
- def cast_boolean(value)
125
- case value.to_s.downcase
126
- when "true", "1"
127
- true
128
- when "false", "0"
129
- false
130
- else
131
- handle_error(:boolean, value: value)
132
- end
133
- end
134
-
135
- # Validates element type for arrays, if `enforce_array_of_type` is enabled.
136
- #
137
- # @param value [Object]
138
- # @return [void]
139
- def validate_element_type!(value)
140
- return unless Castkit.configuration.enforce_array_of_type
141
-
142
- validator = Castkit.configuration.validator_for(options[:of])
143
- validator.call(value, options: options, context: "#{field}[]")
144
- end
145
- end
146
- end
147
- end
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../castkit"
4
-
5
- module Castkit
6
- module AttributeExtensions
7
- # Provides centralized handling of attribute casting and validation errors.
8
- #
9
- # The behavior of each error is controlled by configuration flags in `Castkit.configuration`.
10
- module ErrorHandling
11
- # Maps known error types to their handling behavior.
12
- #
13
- # Each entry includes:
14
- # - `:config` – the config flag that determines enforcement
15
- # - `:message` – a lambda that generates an error message
16
- # - `:error` – the error class to raise
17
- #
18
- # @return [Hash<Symbol, Hash>]
19
- ERROR_OPTIONS = {
20
- array_of_type: {
21
- config: :enforce_array_of_type,
22
- message: ->(*_) { "`of:` must be provided for array type" },
23
- error: Castkit::AttributeError
24
- },
25
- primitive: {
26
- config: :enforce_known_primitive_type,
27
- message: ->(_attr, type:) { "unknown primitive type: #{type.inspect}" },
28
- error: Castkit::AttributeError
29
- },
30
- boolean: {
31
- config: :enforce_boolean_casting,
32
- message: ->(_attr, value:) { "must be a boolean, got: #{value.inspect}" },
33
- error: Castkit::AttributeError
34
- },
35
- union: {
36
- config: :enforce_union_match,
37
- message: ->(_attr, types:) { "could not be cast to any of #{types.inspect}" },
38
- error: Castkit::AttributeError
39
- },
40
- access: {
41
- config: :enforce_attribute_access,
42
- message: ->(_attr, mode:) { "invalid access mode `#{mode}`" },
43
- error: Castkit::AttributeError
44
- },
45
- unwrapped: {
46
- config: :enforce_unwrapped_prefix,
47
- message: ->(*_) { "prefix can only be used with unwrapped attribute" },
48
- error: Castkit::AttributeError
49
- },
50
- array_options: {
51
- config: :enforce_array_options,
52
- message: ->(*_) { "array attribute must specify `of:` type" },
53
- error: Castkit::AttributeError
54
- }
55
- }.freeze
56
-
57
- private
58
-
59
- # Handles a validation or casting error based on the provided error key and context.
60
- #
61
- # If the corresponding configuration flag is enabled, an exception is raised.
62
- # Otherwise, a warning is logged and the method returns `nil`.
63
- #
64
- # @param key [Symbol] the type of error (must match a key in ERROR_OPTIONS)
65
- # @param kwargs [Hash] additional values passed to the message lambda
66
- # @option kwargs [Symbol] :context (optional) the attribute context (e.g., field name)
67
- # @return [nil]
68
- # @raise [Castkit::AttributeError] if enforcement is enabled for the given error type
69
- def handle_error(key, **kwargs)
70
- config_key = ERROR_OPTIONS.dig(key, :config)
71
- message_fn = ERROR_OPTIONS.dig(key, :message)
72
- error_class = ERROR_OPTIONS.dig(key, :error) || Castkit::Error
73
-
74
- context = kwargs.delete(:context)
75
- message = message_fn.call(self, **kwargs)
76
- raise error_class.new(message, context: context) if Castkit.configuration.public_send(config_key)
77
-
78
- Castkit.warning "[Castkit] #{message}"
79
- nil
80
- end
81
- end
82
- end
83
- end
@@ -1,131 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../data_object"
4
-
5
- module Castkit
6
- module AttributeExtensions
7
- # Provides access to normalized attribute options and helper predicates.
8
- #
9
- # These methods support Castkit attribute behavior such as default values,
10
- # key mapping, optionality, and structural roles (e.g. composite or unwrapped).
11
- module Options
12
- # Default options for attributes.
13
- #
14
- # @return [Hash<Symbol, Object>]
15
- DEFAULT_OPTIONS = {
16
- required: true,
17
- ignore_nil: false,
18
- ignore_blank: false,
19
- ignore: false,
20
- composite: false,
21
- unwrapped: false,
22
- prefix: nil,
23
- access: %i[read write]
24
- }.freeze
25
-
26
- # Returns the default value for the attribute.
27
- #
28
- # If the default is callable, it is invoked.
29
- #
30
- # @return [Object]
31
- def default
32
- val = @default
33
- val.respond_to?(:call) ? val.call : val
34
- end
35
-
36
- # Returns the serialization/deserialization key.
37
- #
38
- # Falls back to the field name if `:key` is not specified.
39
- #
40
- # @return [Symbol, String]
41
- def key
42
- options[:key] || field
43
- end
44
-
45
- # Returns the key path for accessing nested keys.
46
- #
47
- # Optionally includes alias key paths if `with_aliases` is true.
48
- #
49
- # @param with_aliases [Boolean]
50
- # @return [Array<Array<Symbol>>] nested key paths
51
- def key_path(with_aliases: false)
52
- path = key.to_s.split(".").map(&:to_sym) || []
53
- return path unless with_aliases
54
-
55
- [path] + alias_paths
56
- end
57
-
58
- # Returns all alias key paths as arrays of symbols.
59
- #
60
- # @return [Array<Array<Symbol>>]
61
- def alias_paths
62
- options[:aliases].map { |a| a.to_s.split(".").map(&:to_sym) }
63
- end
64
-
65
- # Whether the attribute is required for object construction.
66
- #
67
- # @return [Boolean]
68
- def required?
69
- options[:required]
70
- end
71
-
72
- # Whether the attribute is optional.
73
- #
74
- # @return [Boolean]
75
- def optional?
76
- !required?
77
- end
78
-
79
- # Whether to ignore `nil` values during serialization.
80
- #
81
- # @return [Boolean]
82
- def ignore_nil?
83
- options[:ignore_nil]
84
- end
85
-
86
- # Whether to ignore blank values (`[]`, `{}`, empty strings) during serialization.
87
- #
88
- # @return [Boolean]
89
- def ignore_blank?
90
- options[:ignore_blank]
91
- end
92
-
93
- # Whether the attribute is a nested Castkit::DataObject.
94
- #
95
- # @return [Boolean]
96
- def dataobject?
97
- Castkit.dataobject?(type)
98
- end
99
-
100
- # Whether the attribute is a collection of Castkit::DataObjects.
101
- #
102
- # @return [Boolean]
103
- def dataobject_collection?
104
- type == :array && Castkit.dataobject?(options[:of])
105
- end
106
-
107
- # Whether the attribute is considered composite (not exposed in serialized output).
108
- #
109
- # @return [Boolean]
110
- def composite?
111
- options[:composite]
112
- end
113
-
114
- # Whether the attribute is unwrapped into the parent object.
115
- #
116
- # Only applies to Castkit::DataObject types.
117
- #
118
- # @return [Boolean]
119
- def unwrapped?
120
- dataobject? && options[:unwrapped]
121
- end
122
-
123
- # Returns the prefix used for unwrapped attributes.
124
- #
125
- # @return [String, nil]
126
- def prefix
127
- options[:prefix]
128
- end
129
- end
130
- end
131
- end
@@ -1,89 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castkit
4
- module AttributeExtensions
5
- # Handles serialization (`dump`) and deserialization (`load`) of attribute values.
6
- #
7
- # Supports primitive types, arrays, and nested Castkit::DataObject instances.
8
- module Serialization
9
- # Serializes a value into a format suitable for output (e.g., JSON or Hash).
10
- #
11
- # If the value is a Castkit::DataObject, a custom serializer is used if configured.
12
- #
13
- # @param value [Object, nil] the value to serialize
14
- # @param visited [Set, nil] used for circular reference detection
15
- # @return [Object, nil] the serialized value
16
- def dump(value, visited: nil)
17
- return value if value.nil?
18
-
19
- if type == :array
20
- Array(value).map { |val| dump_element(val, visited: visited) }
21
- else
22
- dump_element(value, visited: visited)
23
- end
24
- end
25
-
26
- # Deserializes and validates a value during object instantiation.
27
- #
28
- # Applies default value, casts, and runs validators.
29
- #
30
- # @param value [Object] the input value
31
- # @param context [Symbol] the attribute name or context key
32
- # @return [Object] the deserialized and validated value
33
- # @raise [Castkit::AttributeError] if value is required but missing
34
- def load(value, context:)
35
- value = default if value.nil?
36
- return raise_error!("#{field} is required for instantiation") if value.nil? && required?
37
-
38
- value = cast(value)
39
- validate_value!(value, context: context)
40
-
41
- value
42
- end
43
-
44
- private
45
-
46
- # Serializes a single element value.
47
- #
48
- # - Uses a serializer if the value is a Castkit::DataObject.
49
- # - Converts `to_h` if the value is hash-like.
50
- #
51
- # @param value [Object, nil] the element to dump
52
- # @param visited [Set, nil]
53
- # @return [Object, nil]
54
- def dump_element(value, visited: nil)
55
- return value if value.nil? || primitive?(value)
56
-
57
- if value.is_a?(Castkit::DataObject)
58
- serializer = options[:serializer] || value.class.serializer || Castkit::DefaultSerializer
59
- serializer.call(value, visited: visited)
60
- elsif hashable?(value)
61
- value.to_h(visited)
62
- else
63
- value
64
- end
65
- end
66
-
67
- # Checks whether a value is a hashable object suitable for `to_h` dumping.
68
- #
69
- # @param value [Object, nil]
70
- # @return [Boolean]
71
- def hashable?(value)
72
- value.respond_to?(:to_h) && !primitive?(value) && !value.is_a?(Castkit::Attribute)
73
- end
74
-
75
- # Determines if a value is a primitive type.
76
- #
77
- # @param value [Object, nil]
78
- # @return [Boolean]
79
- def primitive?(value)
80
- case value
81
- when String, Symbol, Numeric, TrueClass, FalseClass, NilClass, Hash, Array
82
- true
83
- else
84
- false
85
- end
86
- end
87
- end
88
- end
89
- end
@@ -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