castkit 0.1.2 → 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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +196 -219
  3. data/CHANGELOG.md +42 -0
  4. data/README.md +469 -84
  5. data/lib/castkit/attribute.rb +6 -24
  6. data/lib/castkit/castkit.rb +58 -10
  7. data/lib/castkit/configuration.rb +94 -47
  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 +45 -60
  18. data/lib/castkit/default_serializer.rb +85 -54
  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 -13
  43. data/lib/castkit/attribute_extensions/access.rb +0 -65
  44. data/lib/castkit/attribute_extensions/casting.rb +0 -147
  45. data/lib/castkit/attribute_extensions/error_handling.rb +0 -83
  46. data/lib/castkit/attribute_extensions/options.rb +0 -131
  47. data/lib/castkit/attribute_extensions/serialization.rb +0 -89
  48. data/lib/castkit/attribute_extensions/validation.rb +0 -72
  49. data/lib/castkit/data_object_extensions/config.rb +0 -113
  50. data/lib/castkit/data_object_extensions/deserialization.rb +0 -110
  51. data/lib/castkit/validators.rb +0 -4
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generic"
4
+ require_relative "../validators/numeric_validator"
5
+
6
+ module Castkit
7
+ module Types
8
+ # Type definition for `:integer` attributes.
9
+ #
10
+ # Handles deserialization from raw input (e.g., strings, floats) to Integer,
11
+ # applies optional numeric validation rules (e.g., `min`, `max`), and returns
12
+ # the value unchanged during serialization.
13
+ #
14
+ # This class is used internally by Castkit when an attribute is defined with:
15
+ # `integer :count`
16
+ class Integer < Generic
17
+ # Deserializes the input value to an Integer.
18
+ #
19
+ # @param value [Object]
20
+ # @return [Integer]
21
+ def deserialize(value)
22
+ value.to_i
23
+ end
24
+
25
+ # Serializes the Integer value.
26
+ #
27
+ # @param value [Integer]
28
+ # @return [Integer]
29
+ def serialize(value)
30
+ value
31
+ end
32
+
33
+ # Validates the Integer value using Castkit's NumericValidator.
34
+ #
35
+ # Supports options like `min:` and `max:`.
36
+ #
37
+ # @param value [Object]
38
+ # @param options [Hash] validation options
39
+ # @param context [Symbol, String] attribute context for error messages
40
+ # @return [void]
41
+ def validate!(value, options: {}, context: {})
42
+ Castkit::Validators::NumericValidator.call(value, options: options, context: context)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "generic"
4
+ require_relative "../validators/string_validator"
5
+
6
+ module Castkit
7
+ module Types
8
+ # Type definition for `:string` attributes.
9
+ #
10
+ # Coerces any input to a string using `to_s`, and validates that the resulting value is a `String`.
11
+ # Supports optional format validation via a `:format` option (Regexp or Proc).
12
+ #
13
+ # This class is used internally by Castkit when an attribute is defined with:
14
+ # `string :id`
15
+ class String < Generic
16
+ # Deserializes the value by coercing it to a string using `to_s`.
17
+ #
18
+ # @param value [Object]
19
+ # @return [String]
20
+ def deserialize(value)
21
+ value.to_s
22
+ end
23
+
24
+ # Serializes the value as-is.
25
+ #
26
+ # @param value [String]
27
+ # @return [String]
28
+ def serialize(value)
29
+ value
30
+ end
31
+
32
+ # Validates the value is a `String` and optionally matches a format.
33
+ #
34
+ # @param value [Object]
35
+ # @param options [Hash] validation options (e.g., `format: /regex/`)
36
+ # @param context [Symbol, String]
37
+ # @raise [Castkit::AttributeError] if validation fails
38
+ # @return [void]
39
+ def validate!(value, options: {}, context: {})
40
+ Castkit::Validators::StringValidator.call(value, options: options, context: context)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "types/collection"
4
+ require_relative "types/boolean"
5
+ require_relative "types/date"
6
+ require_relative "types/date_time"
7
+ require_relative "types/float"
8
+ require_relative "types/generic"
9
+ require_relative "types/integer"
10
+ require_relative "types/string"
11
+
12
+ module Castkit
13
+ # Object types supported natively by Castkit.
14
+ module Types; end
15
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module Validators
5
+ # Abstract base class for all attribute validators.
6
+ #
7
+ # Validators ensure that a value conforms to specific rules (e.g., type, format, range).
8
+ # Subclasses must implement the instance method `#call`.
9
+ #
10
+ # @abstract
11
+ class BaseValidator
12
+ class << self
13
+ # Invokes the validator with the given arguments.
14
+ #
15
+ # @param value [Object] the attribute value to validate
16
+ # @param options [Hash] the attribute options (e.g., `min`, `max`, `format`)
17
+ # @param context [Symbol, String, Hash] the attribute name or context for error reporting
18
+ # @return [void]
19
+ # @raise [Castkit::AttributeError] if validation fails
20
+ def call(value, options:, context:)
21
+ new.call(value, options: options, context: context)
22
+ end
23
+ end
24
+
25
+ # Validates the attribute value.
26
+ #
27
+ # @abstract Override in subclasses.
28
+ #
29
+ # @param value [Object] the attribute value to validate
30
+ # @param options [Hash] the attribute options
31
+ # @param context [Symbol, String, Hash] the attribute name or context
32
+ # @return [void]
33
+ # @raise [NotImplementedError] unless implemented in a subclass
34
+ def call(value, options:, context:)
35
+ raise NotImplementedError, "#{self.class.name} must implement `#call`"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../validator"
3
+ require_relative "base_validator"
4
4
 
5
5
  module Castkit
6
6
  module Validators
7
7
  # Validates that a numeric value falls within the allowed range.
8
8
  #
9
9
  # Supports `:min` and `:max` options to enforce bounds.
10
- class NumericValidator < Castkit::Validator
10
+ class NumericValidator < Castkit::Validators::BaseValidator
11
11
  # Validates the numeric value.
12
12
  #
13
13
  # @param value [Numeric] the value to validate
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../validator"
3
+ require_relative "base_validator"
4
4
 
5
5
  module Castkit
6
6
  module Validators
7
7
  # Validates that a value is a String and optionally conforms to a format.
8
8
  #
9
9
  # Supports format validation using a Regexp or a custom Proc.
10
- class StringValidator < Castkit::Validator
10
+ class StringValidator < Castkit::Validators::BaseValidator
11
11
  # Validates the string value.
12
12
  #
13
13
  # @param value [Object] the value to validate
@@ -16,7 +16,7 @@ module Castkit
16
16
  # @raise [Castkit::AttributeError] if value is not a string or fails format validation
17
17
  # @return [void]
18
18
  def call(value, options:, context:)
19
- raise Castkit::AttributeError, "#{context} must be a String" unless value.is_a?(String)
19
+ raise Castkit::AttributeError, "#{context} must be a string" unless value.is_a?(String)
20
20
 
21
21
  return unless options[:format]
22
22
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castkit
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/castkit.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "castkit/version"
4
+ require_relative "castkit/attribute"
5
+ require_relative "castkit/contract"
4
6
  require_relative "castkit/data_object"
5
7
 
6
8
  # Castkit is a lightweight, type-safe data object system for Ruby.
metadata CHANGED
@@ -1,14 +1,14 @@
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.2.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-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -72,24 +72,40 @@ files:
72
72
  - castkit.gemspec
73
73
  - lib/castkit.rb
74
74
  - 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
75
  - lib/castkit/castkit.rb
82
76
  - lib/castkit/configuration.rb
77
+ - lib/castkit/contract.rb
78
+ - lib/castkit/contract/data_object.rb
79
+ - lib/castkit/contract/generic.rb
80
+ - lib/castkit/contract/result.rb
81
+ - lib/castkit/contract/validator.rb
82
+ - lib/castkit/core/attribute_types.rb
83
+ - lib/castkit/core/attributes.rb
84
+ - lib/castkit/core/config.rb
85
+ - lib/castkit/core/registerable.rb
83
86
  - 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
87
  - lib/castkit/default_serializer.rb
89
88
  - lib/castkit/error.rb
89
+ - lib/castkit/ext/attribute/access.rb
90
+ - lib/castkit/ext/attribute/error_handling.rb
91
+ - lib/castkit/ext/attribute/options.rb
92
+ - lib/castkit/ext/attribute/validation.rb
93
+ - lib/castkit/ext/data_object/contract.rb
94
+ - lib/castkit/ext/data_object/deserialization.rb
95
+ - lib/castkit/ext/data_object/serialization.rb
96
+ - lib/castkit/inflector.rb
90
97
  - lib/castkit/serializer.rb
98
+ - lib/castkit/types.rb
99
+ - lib/castkit/types/boolean.rb
100
+ - lib/castkit/types/collection.rb
101
+ - lib/castkit/types/date.rb
102
+ - lib/castkit/types/date_time.rb
103
+ - lib/castkit/types/float.rb
104
+ - lib/castkit/types/generic.rb
105
+ - lib/castkit/types/integer.rb
106
+ - lib/castkit/types/string.rb
91
107
  - lib/castkit/validator.rb
92
- - lib/castkit/validators.rb
108
+ - lib/castkit/validators/base_validator.rb
93
109
  - lib/castkit/validators/numeric_validator.rb
94
110
  - lib/castkit/validators/string_validator.rb
95
111
  - lib/castkit/version.rb
@@ -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