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,23 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "error"
4
- require_relative "data_object"
5
- require_relative "attribute_extensions/options"
6
- require_relative "attribute_extensions/casting"
7
- require_relative "attribute_extensions/access"
8
- require_relative "attribute_extensions/validation"
9
- require_relative "attribute_extensions/serialization"
4
+ require_relative "ext/attribute/options"
5
+ require_relative "ext/attribute/access"
6
+ require_relative "ext/attribute/validation"
10
7
 
11
8
  module Castkit
12
9
  # Represents a typed attribute on a Castkit::DataObject.
13
10
  #
14
11
  # Provides casting, validation, access control, and serialization behavior.
15
12
  class Attribute
16
- include Castkit::AttributeExtensions::Options
17
- include Castkit::AttributeExtensions::Casting
18
- include Castkit::AttributeExtensions::Access
19
- include Castkit::AttributeExtensions::Validation
20
- include Castkit::AttributeExtensions::Serialization
13
+ include Castkit::Ext::Attribute::Options
14
+ include Castkit::Ext::Attribute::Access
15
+ include Castkit::Ext::Attribute::Validation
21
16
 
22
17
  # @return [Symbol] the attribute name
23
18
  attr_reader :field
@@ -106,19 +101,6 @@ module Castkit
106
101
  end
107
102
  end
108
103
 
109
- # Validates the final value against a validator if required.
110
- #
111
- # @param value [Object]
112
- # @param context [Symbol, String]
113
- # @return [void]
114
- def validate_value!(value, context:)
115
- return if value.nil? && optional?
116
- return if type.is_a?(Array) || dataobject?
117
-
118
- validator = options[:validator] || Castkit.configuration.validator_for(type)
119
- validator&.call(value, options: options, context: context)
120
- end
121
-
122
104
  # Raises a Castkit::AttributeError with optional context.
123
105
  #
124
106
  # @param message [String]
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "configuration"
4
+ require_relative "inflector"
4
5
 
5
6
  # Castkit is a lightweight, type-safe data object system for Ruby.
6
7
  #
@@ -15,30 +16,89 @@ require_relative "configuration"
15
16
  #
16
17
  # user = UserDto.new(name: "Alice", age: 30)
17
18
  # user.to_h #=> { name: "Alice", age: 30 }
19
+ #
20
+ # @see Castkit::Contract
21
+ # @see Castkit::DataObject
18
22
  module Castkit
23
+ # Namespace used for registering DataObjects generated from contracts.
24
+ module DataObjects; end
25
+
26
+ # Namespace used for registering contracts generated from DataObjects.
27
+ module Contracts; end
28
+
19
29
  class << self
20
30
  # Yields the global configuration object for customization.
21
31
  #
22
- # @example
32
+ # @example Disabling array enforcement
23
33
  # Castkit.configure do |config|
24
- # config.enforce_boolean_casting = false
34
+ # config.enforce_typing = false
25
35
  # end
26
36
  #
27
- # @yieldparam config [Castkit::Configuration]
37
+ # @yieldparam config [Castkit::Configuration] the mutable config object
28
38
  # @return [void]
29
39
  def configure
30
40
  yield(configuration)
31
41
  end
32
42
 
33
- # Retrieves the global Castkit configuration.
43
+ # Retrieves the global Castkit configuration instance.
34
44
  #
35
- # @return [Castkit::Configuration] the configuration instance
45
+ # @return [Castkit::Configuration] the configuration object
36
46
  def configuration
37
47
  @configuration ||= Configuration.new
38
48
  end
39
49
 
50
+ # Emits a warning to STDERR if `enable_warnings` is enabled in config.
51
+ #
52
+ # @param message [String] the warning message
53
+ # @return [void]
54
+ def warning(message)
55
+ warn message if configuration.enable_warnings
56
+ end
57
+
58
+ # Checks whether a given object is a subclass of Castkit::DataObject.
59
+ #
60
+ # @param obj [Object] the object to test
61
+ # @return [Boolean] true if obj is a Castkit::DataObject class
40
62
  def dataobject?(obj)
41
63
  obj.is_a?(Class) && obj.ancestors.include?(Castkit::DataObject)
42
64
  end
65
+
66
+ # Returns a type caster lambda for the given type.
67
+ #
68
+ # Type casting performs both validation and deserialization on the provided value.
69
+ #
70
+ # @param type [Symbol] the registered type (e.g. :string)
71
+ # @return [Proc] a lambda that accepts a value and options and returns a casted result
72
+ def type_caster(type)
73
+ type_definition = configuration.fetch_type(type)
74
+
75
+ lambda do |value, validator: nil, options: {}, context: nil|
76
+ type_definition.class.cast!(value, validator: validator, options: options, context: context)
77
+ end
78
+ end
79
+
80
+ # Returns a serializer lambda for the given type.
81
+ #
82
+ # @param type [Symbol] the registered type (e.g. :string)
83
+ # @return [Proc] a lambda that calls `.serialize` on the type
84
+ def type_serializer(type)
85
+ ->(value) { configuration.fetch_type(type).serialize(value) }
86
+ end
87
+
88
+ # Returns a deserializer lambda for the given type.
89
+ #
90
+ # @param type [Symbol] the registered type (e.g. :string)
91
+ # @return [Proc] a lambda that calls `.deserialize` on the type
92
+ def type_deserializer(type)
93
+ ->(value) { configuration.fetch_type(type).deserialize(value) }
94
+ end
95
+
96
+ # Returns a validator lambda for the given type.
97
+ #
98
+ # @param type [Symbol] the registered type (e.g. :string)
99
+ # @return [Proc] a lambda that calls `.validate!` on the type
100
+ def type_validator(type)
101
+ ->(value) { configuration.fetch_type(type).validate!(value) }
102
+ end
43
103
  end
44
104
  end
@@ -1,39 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "validators"
3
+ require_relative "types"
4
4
 
5
5
  module Castkit
6
6
  # Configuration container for global Castkit settings.
7
7
  #
8
- # This includes validator registration and enforcement flags for various runtime checks.
8
+ # This includes type registration, validation, and enforcement flags
9
+ # used throughout Castkit's attribute system.
9
10
  class Configuration
10
- # Default mapping of primitive types to validators.
11
+ # Default mapping of primitive type definitions.
11
12
  #
12
- # @return [Hash<Symbol, Class>]
13
- DEFAULT_VALIDATORS = {
14
- string: Castkit::Validators::StringValidator,
15
- integer: Castkit::Validators::NumericValidator,
16
- float: Castkit::Validators::NumericValidator
13
+ # @return [Hash{Symbol => Castkit::Types::Generic}]
14
+ DEFAULT_TYPES = {
15
+ array: Castkit::Types::Collection.new,
16
+ boolean: Castkit::Types::Boolean.new,
17
+ date: Castkit::Types::Date.new,
18
+ datetime: Castkit::Types::DateTime.new,
19
+ float: Castkit::Types::Float.new,
20
+ hash: Castkit::Types::Generic.new,
21
+ integer: Castkit::Types::Integer.new,
22
+ string: Castkit::Types::String.new
17
23
  }.freeze
18
24
 
19
- # @return [Hash<Symbol, #call>] registered validators by type
20
- attr_reader :validators
21
-
22
- # Whether to raise an error if `of:` is missing for array types.
23
- # @return [Boolean]
24
- attr_accessor :enforce_array_of_type
25
-
26
- # Whether to raise an error for unrecognized primitive types.
27
- # @return [Boolean]
28
- attr_accessor :enforce_known_primitive_type
25
+ # Type aliases for primitive type definitions.
26
+ #
27
+ # @return [Hash{Symbol => Symbol}]
28
+ TYPE_ALIASES = {
29
+ collection: :array,
30
+ bool: :boolean,
31
+ int: :integer,
32
+ map: :hash,
33
+ number: :float,
34
+ str: :string,
35
+ timestamp: :datetime,
36
+ uuid: :string
37
+ }.freeze
29
38
 
30
- # Whether to raise an error on invalid boolean coercion.
31
- # @return [Boolean]
32
- attr_accessor :enforce_boolean_casting
39
+ # @return [Hash{Symbol => Castkit::Types::Generic}] registered types
40
+ attr_reader :types
33
41
 
34
- # Whether to raise an error if a union type has no matching candidate.
42
+ # Whether to raise an error if values should be validated before deserializing, e.g. true -> "true"
35
43
  # @return [Boolean]
36
- attr_accessor :enforce_union_match
44
+ attr_accessor :enforce_typing
37
45
 
38
46
  # Whether to raise an error if access mode is not recognized.
39
47
  # @return [Boolean]
@@ -47,50 +55,94 @@ module Castkit
47
55
  # @return [Boolean]
48
56
  attr_accessor :enforce_array_options
49
57
 
50
- # Initializes the configuration with default validators and enforcement settings.
58
+ # Whether to raise an error for unknown and invalid type definitions.
59
+ # @return [Boolean]
60
+ attr_accessor :raise_type_errors
61
+
62
+ # Whether to emit warnings when Castkit detects misconfigurations.
63
+ # @return [Boolean]
64
+ attr_accessor :enable_warnings
65
+
66
+ # Whether the strict flag is enabled by default for all DataObjects and Contracts.
67
+ # @return [Boolean]
68
+ attr_accessor :strict_by_default
69
+
70
+ # Initializes the configuration with default types and enforcement flags.
51
71
  #
52
72
  # @return [void]
53
73
  def initialize
54
- @validators = DEFAULT_VALIDATORS.dup
55
- @enforce_array_of_type = true
56
- @enforce_known_primitive_type = true
57
- @enforce_boolean_casting = true
58
- @enforce_union_match = true
74
+ @types = DEFAULT_TYPES.dup
75
+ @enforce_typing = true
59
76
  @enforce_attribute_access = true
60
77
  @enforce_unwrapped_prefix = true
61
78
  @enforce_array_options = true
79
+ @raise_type_errors = true
80
+ @enable_warnings = true
81
+ @strict_by_default = true
82
+
83
+ apply_type_aliases!
62
84
  end
63
85
 
64
- # Registers a custom validator for a given type.
86
+ # Registers a new type definition.
65
87
  #
66
- # @param type [Symbol] the type symbol (e.g., :string, :integer)
67
- # @param validator [#call] a callable object that implements `call(value, options:, context:)`
68
- # @param override [Boolean] whether to override an existing validator
69
- # @raise [Castkit::Error] if validator does not respond to `.call`
88
+ # @param type [Symbol] the symbolic type name (e.g., :uuid)
89
+ # @param klass [Class<Castkit::Types::Generic>] the class to register
90
+ # @param override [Boolean] whether to allow overwriting existing registration
91
+ # @raise [Castkit::TypeError] if the type class is invalid or not a subclass of Generic
70
92
  # @return [void]
71
- def register_validator(type, validator, override: false)
72
- return if @validators.key?(type.to_sym) && !override
93
+ def register_type(type, klass, aliases: [], override: false)
94
+ type = type.to_sym
95
+ return if types.key?(type) && !override
73
96
 
74
- unless validator.respond_to?(:call)
75
- raise Castkit::Error, "Validator for `#{type}` must respond to `.call(value, options:, context:)`"
97
+ instance = klass.new
98
+ unless instance.is_a?(Castkit::Types::Generic)
99
+ raise Castkit::TypeError, "Expected subclass of Castkit::Types::Generic for `#{type}`"
76
100
  end
77
101
 
78
- @validators[type.to_sym] = validator
102
+ types[type] = instance
103
+
104
+ Castkit::Core::AttributeTypes.define_type_dsl(type) if Castkit::Core::AttributeTypes.respond_to?(:define_type_dsl)
105
+ return unless aliases.any?
106
+
107
+ aliases.each { |alias_type| register_type(alias_type, klass, override: override) }
108
+ end
109
+
110
+ # Returns the type handler for a given type symbol.
111
+ #
112
+ # @param type [Symbol]
113
+ # @return [Castkit::Types::Generic]
114
+ # @raise [Castkit::TypeError] if the type is not registered
115
+ def fetch_type(type)
116
+ @types.fetch(type.to_sym) do
117
+ raise Castkit::TypeError, "Unknown type `#{type.inspect}`" if raise_type_errors
118
+ end
79
119
  end
80
120
 
81
- # Returns the registered validator for the given type.
121
+ # Returns whether a type is currently registered.
82
122
  #
83
123
  # @param type [Symbol]
84
- # @return [#call, nil]
85
- def validator_for(type)
86
- validators[type.to_sym]
124
+ # @return [Boolean]
125
+ def type_registered?(type)
126
+ @types.key?(type.to_sym)
87
127
  end
88
128
 
89
- # Resets all validators to their default mappings.
129
+ # Restores the type registry to its default state.
90
130
  #
91
131
  # @return [void]
92
- def reset_validators!
93
- @validators = DEFAULT_VALIDATORS.dup
132
+ def reset_types!
133
+ @types = DEFAULT_TYPES.dup
134
+ apply_type_aliases!
135
+ end
136
+
137
+ private
138
+
139
+ # Registers aliases for primitive type definitions.
140
+ #
141
+ # @return [void]
142
+ def apply_type_aliases!
143
+ TYPE_ALIASES.each do |alias_key, canonical|
144
+ register_type(alias_key, DEFAULT_TYPES[canonical].class)
145
+ end
94
146
  end
95
147
  end
96
148
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module Contract
5
+ # Extension module that adds `.to_dataobject` and `.dataobject` support to Castkit contracts.
6
+ #
7
+ # This allows any contract to be dynamically converted into a Castkit::DataObject class,
8
+ # enabling reuse of validation schemas for serialization, coercion, or API response modeling.
9
+ #
10
+ # This module is automatically included by Castkit contract classes and is not
11
+ # intended to be used manually.
12
+ #
13
+ # @example
14
+ # contract = Castkit::Contract.build(:user) do
15
+ # string :id
16
+ # string :email
17
+ # end
18
+ #
19
+ # UserDto = contract.to_dataobject
20
+ # UserDto.new(id: "123", email: "a@example.com")
21
+ module DataObject
22
+ # Returns or builds a Castkit::DataObject from the current contract.
23
+ #
24
+ # Memoizes the result to avoid repeated regeneration.
25
+ #
26
+ # @example
27
+ # contract = Castkit::Contract.build(:user) do
28
+ # string :id
29
+ # string :name
30
+ # end
31
+ #
32
+ # dto_class = contract.dataobject
33
+ # dto = dto_class.new(id: "123", name: "Alice")
34
+ #
35
+ # @return [Class<Castkit::DataObject>] the generated DTO class
36
+ def dataobject
37
+ @dataobject ||= to_dataobject
38
+ end
39
+
40
+ # Constructs an ephemeral Castkit::DataObject class from the current contract.
41
+ #
42
+ # This creates a new anonymous class each time unless memoized via {#dataobject}.
43
+ #
44
+ # @example
45
+ # dto_class = contract.to_dataobject
46
+ #
47
+ # @return [Class<Castkit::DataObject>] the dynamically generated DTO
48
+ def to_dataobject
49
+ Class.new(Castkit::DataObject).tap do |klass|
50
+ attributes.each_value do |attr|
51
+ klass.attribute(attr.field, attr.type, **attr.options)
52
+ end
53
+ end
54
+ end
55
+
56
+ # Alias for {#to_dataobject}
57
+ #
58
+ # @see #to_dataobject
59
+ alias to_dto to_dataobject
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../core/config"
4
+ require_relative "../core/attribute_types"
5
+ require_relative "../core/registerable"
6
+ require_relative "result"
7
+
8
+ module Castkit
9
+ module Contract
10
+ # Base class for all Castkit contracts.
11
+ #
12
+ # Castkit contracts define validation logic over a set of attributes using a DSL.
13
+ # You can either subclass this directly or use {Castkit::Contract.build} to generate
14
+ # ephemeral or reusable contract classes.
15
+ #
16
+ # @example Subclassing directly
17
+ # class MyContract < Castkit::Contract::Generic
18
+ # string :id
19
+ # integer :count, required: false
20
+ # end
21
+ #
22
+ # MyContract.validate!(id: "abc")
23
+ #
24
+ # @example Using Contract.build (preferred for dynamic generation)
25
+ # UserContract = Castkit::Contract.build(:user) do
26
+ # string :id
27
+ # string :email, required: false
28
+ # end
29
+ #
30
+ # UserContract.validate!(id: "123")
31
+ #
32
+ # @see Castkit::Contract.build
33
+ class Generic
34
+ extend Castkit::Core::Config
35
+ extend Castkit::Core::AttributeTypes
36
+ extend Castkit::Core::Registerable
37
+
38
+ ATTRIBUTE_OPTIONS = %i[
39
+ required aliases min max format of validator unwrapped prefix force_type
40
+ ].freeze
41
+
42
+ class << self
43
+ # Registers the current class under `Castkit::Contracts`.
44
+ #
45
+ # @param as [String, Symbol, nil] The constant name to use (PascalCase). Defaults to the name used when building
46
+ # the contract. If no name was provided, an error is raised.
47
+ # @return [Class] the registered contract class
48
+ # @raise [Castkit::Error] If a name cannot be resolved.
49
+ def register!(as: nil)
50
+ super(namespace: :contracts, as: as || definition[:name])
51
+ end
52
+
53
+ # Defines an attribute for the contract.
54
+ #
55
+ # Only a subset of options is allowed inside a contract.
56
+ #
57
+ # @param field [Symbol] the field name
58
+ # @param type [Symbol, Class, Array] the type or union of types
59
+ # @param options [Hash] allowed options like :required or :validator
60
+ # @return [void]
61
+ def attribute(field, type, **options)
62
+ options = options.slice(*ATTRIBUTE_OPTIONS)
63
+ attributes[field] = Castkit::Attribute.new(field, type, **options)
64
+ end
65
+
66
+ # Validates input against the contract and returns a Result.
67
+ #
68
+ # @param input [Hash]
69
+ # @return [Castkit::Contract::Result]
70
+ def validate(input)
71
+ validate!(input)
72
+ Castkit::Contract::Result.new(definition[:name].to_s, input)
73
+ rescue Castkit::ContractError => e
74
+ Castkit::Contract::Result.new(definition[:name].to_s, input, errors: e.errors)
75
+ end
76
+
77
+ # Validates input and raises on failure.
78
+ #
79
+ # @param input [Hash]
80
+ # @raise [Castkit::ContractError]
81
+ # @return [void]
82
+ def validate!(input)
83
+ Castkit::Contract::Validator.call!(attributes.values, input, **validation_rules)
84
+ end
85
+
86
+ # Returns internal contract metadata.
87
+ #
88
+ # @return [Hash]
89
+ def definition
90
+ @definition ||= {
91
+ name: :ephemeral_contract,
92
+ attributes: {}
93
+ }
94
+ end
95
+
96
+ # Returns the defined attributes.
97
+ #
98
+ # @return [Hash{Symbol => Castkit::Attribute}]
99
+ def attributes
100
+ definition[:attributes]
101
+ end
102
+
103
+ private
104
+
105
+ # Defines the contract from a source or block.
106
+ #
107
+ # @param name [Symbol, String]
108
+ # @param source [Castkit::DataObject, nil]
109
+ # @param block [Proc, nil]
110
+ # @return [Hash]
111
+ def define(name = :ephemeral_contract, source = nil, validation_rules: {}, &block)
112
+ validate_definition!(source, &block)
113
+
114
+ if source
115
+ define_from_source(name, source)
116
+ else
117
+ define_from_block(name, &block)
118
+ end
119
+
120
+ validation_rules.each { |k, v| self.validation_rules[k] = v }
121
+ attributes
122
+ end
123
+
124
+ # Copies attributes from a DataObject.
125
+ #
126
+ # @param name [Symbol, String]
127
+ # @param source [Castkit::DataObject]
128
+ # @return [void]
129
+ def define_from_source(name, source)
130
+ source_attributes = source.attributes.dup
131
+
132
+ @definition = {
133
+ name: name,
134
+ attributes: source_attributes.transform_values do |attr|
135
+ Castkit::Attribute.new(attr.field, attr.type, **attr.options.slice(*ATTRIBUTE_OPTIONS))
136
+ end
137
+ }
138
+ end
139
+
140
+ # Executes DSL block in the contract context.
141
+ #
142
+ # @param name [Symbol, String]
143
+ # @yield [block]
144
+ # @return [void]
145
+ def define_from_block(name, &block)
146
+ definition[:name] = name
147
+
148
+ @__castkit_contract_dsl = true
149
+ instance_eval(&block)
150
+ ensure
151
+ @__castkit_contract_dsl = false
152
+ end
153
+
154
+ # Ensures a valid contract definition input.
155
+ #
156
+ # @param source [Object, nil]
157
+ # @raise [Castkit::ContractError]
158
+ # @return [void]
159
+ def validate_definition!(source)
160
+ raise Castkit::ContractError, "Received both source and block" if source && block_given?
161
+ return if block_given? || Castkit.dataobject?(source)
162
+
163
+ raise Castkit::ContractError, "Expected a Castkit::DataObject or contract block"
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castkit
4
+ module Contract
5
+ # Represents the result of a contract validation.
6
+ #
7
+ # Provides access to the validation outcome, including whether it succeeded or failed,
8
+ # and includes the full list of errors if any.
9
+ class Result
10
+ # @return [Symbol] the name of the contract
11
+ attr_reader :contract
12
+
13
+ # @return [Hash{Symbol => Object}] the validated input
14
+ attr_reader :input
15
+
16
+ # @return [Hash{Symbol => Object}] the validation error hash
17
+ attr_reader :errors
18
+
19
+ # Initializes a new result object.
20
+ #
21
+ # @param contract [Symbol, String] the name of the contract
22
+ # @param input [Hash{Symbol => Object}] the validated input
23
+ # @param errors [Hash{Symbol => Object}] the validation errors
24
+ def initialize(contract, input, errors: {})
25
+ @contract = contract.to_sym.freeze
26
+ @input = input.freeze
27
+ @errors = errors.freeze
28
+ end
29
+
30
+ # A debug-friendly representation of the validation result.
31
+ #
32
+ # @return [String]
33
+ def inspect
34
+ "#<#{self.class.name} contract=#{contract.inspect} success=#{success?} errors=#{errors.inspect}>"
35
+ end
36
+
37
+ # Whether the validation passed with no errors.
38
+ #
39
+ # @return [Boolean]
40
+ def success?
41
+ errors.empty?
42
+ end
43
+
44
+ # Whether the validation failed with one or more errors.
45
+ #
46
+ # @return [Boolean]
47
+ def failure?
48
+ !success?
49
+ end
50
+
51
+ # A readable string representation of the validation result.
52
+ #
53
+ # @return [String]
54
+ def to_s
55
+ return "[Castkit] Contract validation passed for #{contract.inspect}" if success?
56
+
57
+ parsed_errors = errors.map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
58
+ "[Castkit] Contract validation failed for #{contract.inspect}:\n#{parsed_errors}"
59
+ end
60
+
61
+ # @return [Hash{Symbol => Object}] the input validation and error hash
62
+ def to_hash
63
+ @to_hash ||= {
64
+ contract: contract,
65
+ input: input,
66
+ errors: errors
67
+ }.freeze
68
+ end
69
+
70
+ # @return [Hash{Symbol => Object}] the input and validation error hash
71
+ alias to_h to_hash
72
+ end
73
+ end
74
+ end